diff options
72 files changed, 4811 insertions, 668 deletions
@@ -1,105 +1,7 @@ -SHELL := /bin/bash -# Shared packaging target; included after chapter source variables below. -.DELETE_ON_ERROR: +# Standalone compiler snapshot; shared rules also work in linearized checkouts. +include $(firstword $(wildcard ../build-support/early-chapter.mk build-support/early-chapter.mk)) +test_cp += com.seaofnodes.simple.FuzzerWrap -# 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)) -include $(firstword $(wildcard ../graph/graph.mk graph/graph.mk)) -test_classes := $(patsubst $(TST)/%java,$(CLZDIR)/test/%class,$(test_javas)) -test_cp := $(patsubst $(TST)/$(SIMPLE)/%.java,com.seaofnodes.simple.%,$(wildcard $(TST)/$(SIMPLE)/*Test.java)) -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 $(graph_javas) - @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) - @echo "testing " $(test_cp) - @$(JVM) org.junit.runner.JUnitCore $(test_cp) com.seaofnodes.simple.FuzzerWrap - -fuzzer: $(default_targets) - @echo "testing " $(test_cp) - @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap - - -.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) - -include $(firstword $(wildcard ../build-support/chapter-release.mk build-support/chapter-release.mk)) - -.PHONY: build -build: $(main_classes) +.PHONY: fuzzer +fuzzer: $(test_classes) + java -ea -cp "$(CLZDIR)/main$(SEP)$(CLZDIR)/test$(SEP)$(jars)" org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap @@ -51,7 +51,7 @@ The Simple language is styled after a subset of C or Java. * [Chapter 7](docs/chapter07/README.md): `while` statement; looping constructs - eager phi approach. * [Chapter 8](docs/chapter08/README.md): Looping constructs continued, lazy phi creation, `break` and `continue` statements. * [Chapter 9](docs/chapter09/README.md): Global Value Numbering. Iterative peepholes to fixpoint. Worklists. -* Chapter 10a: User defined structs, pointers and null +* [Chapter 10a](docs/chapter10a/README.md): User defined structs, pointers and null analysis. One memory value in SSA. Loads, stores, and an executable evaluator. * Chapter 10b: Equivalence class aliasing. Lazy memory partitioning with `MemMerge`, `MemPhi`, and `BulkMemPhi`. diff --git a/chapter10a.iml b/chapter10a.iml new file mode 100644 index 00000000..3f930463 --- /dev/null +++ b/chapter10a.iml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="false"> + <output url="file://$MODULE_DIR$/../build/idea/production/chapter10a" /> + <output-test url="file://$MODULE_DIR$/../build/idea/test/chapter10a" /> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" /> + <excludeFolder url="file://$MODULE_DIR$/build" /> + <excludeFolder url="file://$MODULE_DIR$/target" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="graph" /> + <orderEntry type="module-library" scope="TEST"> + <library name="JUnit4"> + <CLASSES> + <root url="jar://$MODULE_DIR$/lib/junit-4.12.jar!/" /> + <root url="jar://$MODULE_DIR$/lib/hamcrest-core-1.3.jar!/" /> + </CLASSES> + <JAVADOC /> + <SOURCES /> + </library> + </orderEntry> + </component> +</module> diff --git a/docs/chapter10a/10-grammar.md b/docs/chapter10a/10-grammar.md new file mode 100644 index 00000000..5b9b680e --- /dev/null +++ b/docs/chapter10a/10-grammar.md @@ -0,0 +1,125 @@ +# Grammar for Chapter 10a + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | structDeclaration + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatment + ; + +field + : 'int' IDENTIFIER ';' + ; + +fields + : field+ + ; + +structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + | fieldExpression '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +structName + : IDENTIFIER + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + | structName ('?')? IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression + ; + +newExpression + : 'new' IDENTIFIER + ; + +fieldExpression + : primaryExpresson '.' IDENTIFIER + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | 'null' + | newExpression + | '(' expression ')' + | fieldExpression + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/docs/chapter10a/Makefile b/docs/chapter10a/Makefile new file mode 100644 index 00000000..055882b2 --- /dev/null +++ b/docs/chapter10a/Makefile @@ -0,0 +1,14 @@ +GV := $(wildcard *.gv) +SVG := $(patsubst %.gv,%.svg,$(GV)) +.DELETE_ON_ERROR: + +.PHONY: all +all: $(SVG) + +%.svg: %.gv + dot -Tsvg $< > $@ + @sed -i 's/\r$$//' $@ + +.PHONY: clean +clean: + @rm -f $(SVG) diff --git a/docs/chapter10a/README.md b/docs/chapter10a/README.md new file mode 100644 index 00000000..8aaf779c --- /dev/null +++ b/docs/chapter10a/README.md @@ -0,0 +1,290 @@ +# Chapter 10a: Structs and Memory + +[Previous: Chapter 9](../chapter09/README.md) | +[Next: Chapter 10b](../chapter10b/README.md) + +This chapter adds user-defined structs, pointers, allocation, field loads and +stores, and null checking. It also introduces an evaluator that executes the +graph, including its memory effects. The [grammar](10-grammar.md) is the +same in 10a and 10b; the next chapter improves the memory representation. + +## One memory value + +Arithmetic nodes depend on the values they use, similarly a load also depends +on when it observes memory. In this chapter every store consumes the preceding +memory state and produces the next one, and a load consumes memory without +changing it. This makes the source program's memory ordering explicit in the +graph. + +```java +struct Point { int x; int y; } +Point p = new Point; +p.x = 3; +p.y = arg; +return p.x; +``` + +`NewNode` produces the pointer. The parser emits stores of zero for both fields, +then stores 3 into `x` and `arg` into `y`. The final load observes the memory +after the `y` store. We deliberately keep that dependency here, even though +different fields are independent. Chapter 10b will remove it. + + + +| Node | Inputs | Result | +|----------|---|---| +| `New` | Control and a struct type | A fresh, non-null object pointer | +| `Store` | Memory, pointer, field name, value | The updated whole-memory state | +| `Load` | Memory, pointer, field name | The value of the field | +| `Start` | External entry | Control, initial memory, and `arg` | +| `Return` | Control, result, memory | Completion with all preceding effects retained | + +The memory edge is a dependency, not a copy of the heap. The graph determines +the legal execution order. In hardware, a Store updates physical memory +directly. In the evaluator, a Store updates an objects' attached memory array. + +## Memory is an SSA variable + +The parser keeps one hidden binding named `$mem`. Source identifiers cannot +contain `$`, so user code cannot name it. Reading and updating memory uses the +same `ScopeNode` lookup and update operations as a local variable. + +For an `if`, the two branches can finish with different memory values. An +ordinary Phi selects the memory from the branch that executed: + + + +Loops reuse Chapter 8's lazy Phi construction. Reading or updating `$mem` in a +loop creates an initially incomplete memory Phi. Closing the loop supplies its +backedge. `break`, `continue`, nested loops, and early returns use the same scope +mechanics as scalar variables. + + + +Every Return consumes the current whole-memory value. This keeps stores alive +even when the returned scalar does not depend on them, or when the program +returns the modified object itself. + +## Local memory optimizations + +A load immediately following a store to the **same pointer and field** can use +the stored value. A store immediately following another store to the same +address can discard the earlier store when no other user needs it. Both the +pointer and field checks matter: two different fields of one object are +different addresses, as are the same fields of two different objects. + +A load can also move through a memory Phi when doing so exposes a useful +load-after-store fold. On a loop, the backedge must fold, preventing this rewrite +from repeatedly moving the load around the cycle. + +The single chain is conservative. A store to `y` can prevent forwarding an +earlier store to `x`, even when the objects are identical. This is a missed +optimization, not a correctness problem; it motivates the next chapter. + +## Enhanced Type Lattice + +The Type Lattice for Simple has a major revision in this chapter. + + + +The diagram uses `⊤` for **top** and `⊥` for **bottom**. A suffix names the +domain: `⊤:struct` is *struct top*, while the bare `⊤` is global top. In the +prose we use those words; in code examples we use the actual Java constants. +The implementation's `$TOP` and `$BOT` names need not appear in the diagram: + +| Diagram | Prose | Java constant | Internal struct name | +|---|---|---|---| +| `⊤:struct` | struct top | `TypeStruct.TOP` | `$TOP` | +| `⊥:struct` | struct bottom | `TypeStruct.BOT` | `$BOT` | + +Edges in this diagram show lattice order, with top above bottom. They are not +the use-to-definition edges of the program graphs elsewhere in the chapter. +Boxes containing an ellipsis abbreviate further parallel alternatives; they +are not extra union types. Field and tuple-element type lattices are elided. + +Within the type lattice, we now have the following domains: + +*  represents control flow: unreachable control + at control top, and reachable control at control bottom. +*  has integer top, integer bottom, and parallel + constants between them. The ellipses stand for the other integer constants. +*  (new) combines a struct type with nullability. + * The prefix `*` means pointer-to: `*Person` is a non-null pointer to a Person. + The suffix `?` includes null, so `*Person?` permits a Person pointer or null. + * The pointer domain contains the whole struct lattice twice, once without + null and once with it. The diagram expands named alternatives in both + copies. It is not just two individual pointer types. + * `null` combines struct top with the nullable flag: it has no non-null + object alternative. The diagram labels it `*⊤:struct?` as well as `null`. + * A pointer to struct bottom can point to any struct. The diagram shows the + non-null form as `*⊥:struct` and the nullable form as `*⊥:struct?`. + The latter is pointer bottom; pointer top is `*⊤:struct`. + * Null evaluates to false; non-null pointers evaluate to true. + + > C analogy: the non-null pointer to struct bottom is akin to `void *`, except + > that C's `void *` may also be null. Simple names this constant + > `TypeMemPtr.VOIDPTR` and prints it as `*void`. Struct bottom itself is the + > common lower bound of struct types, not C's `void` type. + +*  (new) represents user-defined named structs. + Fields have integer types in this chapter. + * Struct top lies above every named struct; struct bottom lies below every + named struct. Top is the high/empty struct alternative, while bottom + conservatively includes any struct name. + * `Person`, `s1`, `s2`, and further names are parallel alternatives. There is + no inheritance, so different named structs do not nest beneath one another, + even if they have identical fields. + * There is no fixed bound on the names a program can declare. The diagram's + ellipsis stands for more named alternatives, not a distinguished unnamed + struct. Each named box abbreviates its own field-type lattice. +*  (new) represents the whole-memory token. + Memory top and memory bottom are `TypeMem.TOP` and `TypeMem.BOT`. Neither + records the values stored in individual fields. +*  represents a collection of results from one node, + such as Start's control, memory, and argument. The displayed shapes abbreviate + tuples of different lengths and their element-type lattices. + +We use the following operations on the lattice: + +* **Meet** computes the greatest lower bound. For these examples, think of + combining the alternatives represented by its inputs. + * The meet of `Person` and `s1` is struct bottom. There is no separate type + representing exactly those two names. + * The meet of `*Person` and `*s1` is the non-null pointer to struct bottom. + * The meet of `*Person` and `null` is `*Person?`. + * The meet of the non-null pointer to struct bottom and `null` is pointer bottom. + * The meet of `1` and `2` is integer bottom. +* **Dual** reflects the lattice about its centerline: top and bottom exchange + places, and applying dual twice returns the original type. + * Struct top and struct bottom are duals. + * `null` and the non-null pointer to struct bottom are duals. + * A pointer's dual also duals its struct's fields and flips nullability. Thus + the dual of `*Person` is not generally the original `*Person?`: the field + types must be dualed too. Those field details are omitted in the diagram. +* **Join** computes the least upper bound. It is defined through meet and dual: + `JOIN(x,y) = MEET(x.dual(),y.dual()).dual()`. + * The join of `Person` and `s1` is struct top. + * The join of `*Person` and `*s1` is pointer top. + * The join of the non-null pointer to struct bottom and `null` is pointer top. + * The join of `1` and `2` is integer top. + +As we construct the Sea of Nodes graph, we ensure that values stay inside the +domain they are created in - that is ptr fields will never contain ints +(obviously)... nor even the generic `TOP` and `BOT`. There are a couple of +nuances worth highlighting. + +* When Phis are created, the initial type of the Phi is based on the declared + type of its first input. This is a pessimistic type assignment because we do + not yet know what other types will be met. + +* When all the inputs of the Phi are known, we start with the local Top of the + declared type, and then compute a meet of all the input types. This + computation results in a more refined type for the Phi. + +## Parser Enhancements + +In previous chapters the only available type was Integer. Now, variables can +be of Integer type or pointer to Struct types. To support this, the Parser now +tracks the declared type of a variable (used to only be Integer!). The +declared type of a variable defines the set of values that can be legitimately +assigned to the variable. The Sea of Nodes graph also tracks the actual type +of values assigned to variables, these type transitions are defined by the Type +lattice described in the previous section. + +## Null Pointer Analysis + +The Simple language syntax allows a pointer variable to be specified as +null-able - and it prevents ever throwing a Null Pointer Exception. The +compiler decides if a null check is needed, and if needed and missing +the compiler rejects the program (asking the user to insert a null check). + +When the Parser encounters conditions that test the truthiness of a pointer variable, it uses this knowledge to refine the type information in the branches that follow. + +Here are two motivating examples: + +```java +struct Bar { int a; } +Bar? bar = new Bar; +if (arg) bar = null; +if( bar ) bar.a = 1; +``` + +If `bar` is not `null` above, then we would like to allow the assignment to +`bar.a`. If the final line lacked the null check, e.g. a plain `bar.a = 1;`, +then `Java` would compile this program but throw a NPE, and Simple rejects the +program pointing out a null check is required. + + +```java +struct Bar { int a; } +Bar? bar = new Bar; +if (arg) bar = null; +int rez = 3; +if( !bar ) rez=4; +else bar.a = 1; +``` + +If `bar` is `null` then we would like `!bar` to evaluate to `true`. In the else +branch we know `bar` is not null, hence the assignment to `bar.a` should be +allowed. + +To enable this behaviour, we make following enhancements. + +* We track the null-ness of a pointer in the Type system, and hence in every + Node that ptr type flows through. + +* We "wrap" the predicate of an `If` node with an up-cast when we see that the + predicate is a ptr value. The up-cast removes the null possibility on the + true branch making it legal to use the pointer. + +* The up-cast uses a `Cast` with the non-null pointer to struct bottom + (`TypeMemPtr.VOIDPTR`). It joins the original pointer type with that type, + corresponding to `*⊥:struct` in the diagram. This JOIN operation preserves + everything we already know about ptr, and also adds the new knowledge that + the ptr is not-null. + +* The up-cast is represented by the `CastNode` op, and if applicable, we + replace all occurrences of the original predicate with the up-cast in the + current scope. The change is local to each branch of the `If`; occuring + separately for the true and the false branches of the `If` (and the false + branch inverts the predicate). + +* The up-cast peepholes normally; if its input's type is a super type of the + up-cast, then the `CastNode` can be removed. See `ScopeNode.upcast()` method, + and `CastNode`. + +* When computing a type for Not, we produce an Integer type when its input is a + ptr. When the input is a `null` we convert to `1` and when is a not `null` + ptr value, we convert to `0`. See `NotNode.compute()`. + + +## Executing the graph + +The evaluator includes a small late scheduler. A load must execute before a +later store that consumes the same memory definition, even though there is no +ordinary data edge from the load to that store. These load/store +anti-dependencies supplement the explicit graph edges. + + + +The scheduler is sufficient to execute this chapter. Chapter 11 develops +global code motion and scheduling as a separate topic. + +## Build and test + +From this directory, with JDK 21 or newer, Make, and a shell: + +```sh +make lib +make tests +make release +make view +``` + +Make builds this snapshot directly, without Maven. `make release` produces +`build/release/simple.jar`; `make view` launches the shared interactive graph +viewer. Maven and IDEA module descriptions are also supplied. Tests include all +earlier chapters, memory ordering, returned object contents, and nested loops. + +Next, [Chapter 10b](../chapter10b/README.md) keeps this one parser binding while +splitting the graph into independent memory slices. diff --git a/docs/chapter10a/example1.gv b/docs/chapter10a/example1.gv new file mode 100644 index 00000000..d399b562 --- /dev/null +++ b/docs/chapter10a/example1.gv @@ -0,0 +1,10 @@ +digraph Memory { + rankdir=RL; + node [shape=box]; + M0 [label="Initial memory"]; + X [label="Store p.x = 3"]; + Y [label="Store p.y = arg"]; + L [label="Load p.x"]; + L -> Y -> X -> M0 [color=blue]; + Return -> Y [color=blue]; Return -> L; +} diff --git a/docs/chapter10a/example1.svg b/docs/chapter10a/example1.svg new file mode 100644 index 00000000..d1c5e3bd --- /dev/null +++ b/docs/chapter10a/example1.svg @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 12.2.0 (20241103.1931) + --> +<!-- Title: Memory Pages: 1 --> +<svg width="556pt" height="67pt" + viewBox="0.00 0.00 555.75 67.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 63)"> +<title>Memory</title> +<polygon fill="white" stroke="none" points="-4,4 -4,-63 551.75,-63 551.75,4 -4,4"/> +<!-- M0 --> +<g id="node1" class="node"> +<title>M0</title> +<polygon fill="none" stroke="black" points="98.5,-59 0,-59 0,-23 98.5,-23 98.5,-59"/> +<text text-anchor="middle" x="49.25" y="-35.95" font-family="Times New Roman,serif" font-size="14.00">Initial memory</text> +</g> +<!-- X --> +<g id="node2" class="node"> +<title>X</title> +<polygon fill="none" stroke="black" points="222.5,-59 134.5,-59 134.5,-23 222.5,-23 222.5,-59"/> +<text text-anchor="middle" x="178.5" y="-35.95" font-family="Times New Roman,serif" font-size="14.00">Store p.x = 3</text> +</g> +<!-- X->M0 --> +<g id="edge3" class="edge"> +<title>X->M0</title> +<path fill="none" stroke="blue" d="M134.06,-41C126.33,-41 118.16,-41 110.06,-41"/> +<polygon fill="blue" stroke="blue" points="110.24,-37.5 100.24,-41 110.24,-44.5 110.24,-37.5"/> +</g> +<!-- Y --> +<g id="node3" class="node"> +<title>Y</title> +<polygon fill="none" stroke="black" points="357,-59 258.5,-59 258.5,-23 357,-23 357,-59"/> +<text text-anchor="middle" x="307.75" y="-35.95" font-family="Times New Roman,serif" font-size="14.00">Store p.y = arg</text> +</g> +<!-- Y->X --> +<g id="edge2" class="edge"> +<title>Y->X</title> +<path fill="none" stroke="blue" d="M258.28,-41C250.45,-41 242.29,-41 234.32,-41"/> +<polygon fill="blue" stroke="blue" points="234.35,-37.5 224.35,-41 234.35,-44.5 234.35,-37.5"/> +</g> +<!-- L --> +<g id="node4" class="node"> +<title>L</title> +<polygon fill="none" stroke="black" points="457.75,-36 393,-36 393,0 457.75,0 457.75,-36"/> +<text text-anchor="middle" x="425.38" y="-12.95" font-family="Times New Roman,serif" font-size="14.00">Load p.x</text> +</g> +<!-- L->Y --> +<g id="edge1" class="edge"> +<title>L->Y</title> +<path fill="none" stroke="blue" d="M392.78,-24.28C385.16,-25.8 376.76,-27.47 368.31,-29.15"/> +<polygon fill="blue" stroke="blue" points="367.85,-25.68 358.73,-31.06 369.22,-32.54 367.85,-25.68"/> +</g> +<!-- Return --> +<g id="node5" class="node"> +<title>Return</title> +<polygon fill="none" stroke="black" points="547.75,-59 493.75,-59 493.75,-23 547.75,-23 547.75,-59"/> +<text text-anchor="middle" x="520.75" y="-35.95" font-family="Times New Roman,serif" font-size="14.00">Return</text> +</g> +<!-- Return->Y --> +<g id="edge4" class="edge"> +<title>Return->Y</title> +<path fill="none" stroke="blue" d="M493.52,-43.07C482.49,-43.84 469.5,-44.62 457.75,-45 428.99,-45.93 421.77,-45.68 393,-45 385.14,-44.81 376.87,-44.53 368.71,-44.2"/> +<polygon fill="blue" stroke="blue" points="368.95,-40.7 358.81,-43.76 368.65,-47.7 368.95,-40.7"/> +</g> +<!-- Return->L --> +<g id="edge5" class="edge"> +<title>Return->L</title> +<path fill="none" stroke="black" d="M493.74,-34.59C486.17,-32.73 477.69,-30.64 469.38,-28.59"/> +<polygon fill="black" stroke="black" points="470.24,-25.2 459.7,-26.21 468.57,-32 470.24,-25.2"/> +</g> +</g> +</svg> diff --git a/docs/chapter10a/example2a.gv b/docs/chapter10a/example2a.gv new file mode 100644 index 00000000..ec5afbdc --- /dev/null +++ b/docs/chapter10a/example2a.gv @@ -0,0 +1,10 @@ +digraph Memory { + rankdir=RL; + node [shape=box]; + M [label="Incoming memory"]; + A [label="True: Store x"]; + B [label="False: Store y"]; + P [label="Phi(memory)"]; + P -> A -> M [color=blue]; P -> B -> M [color=blue]; + P -> Region [style=dashed]; Return -> P [color=blue]; +} diff --git a/docs/chapter10a/example2a.svg b/docs/chapter10a/example2a.svg new file mode 100644 index 00000000..f9a9b4fd --- /dev/null +++ b/docs/chapter10a/example2a.svg @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 12.2.0 (20241103.1931) + --> +<!-- Title: Memory Pages: 1 --> +<svg width="469pt" height="152pt" + viewBox="0.00 0.00 469.25 152.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 148)"> +<title>Memory</title> +<polygon fill="white" stroke="none" points="-4,4 -4,-148 465.25,-148 465.25,4 -4,4"/> +<!-- M --> +<g id="node1" class="node"> +<title>M</title> +<polygon fill="none" stroke="black" points="118.75,-117 0,-117 0,-81 118.75,-81 118.75,-117"/> +<text text-anchor="middle" x="59.38" y="-93.95" font-family="Times New Roman,serif" font-size="14.00">Incoming memory</text> +</g> +<!-- A --> +<g id="node2" class="node"> +<title>A</title> +<polygon fill="none" stroke="black" points="244.25,-144 156.25,-144 156.25,-108 244.25,-108 244.25,-144"/> +<text text-anchor="middle" x="200.25" y="-120.95" font-family="Times New Roman,serif" font-size="14.00">True: Store x</text> +</g> +<!-- A->M --> +<g id="edge2" class="edge"> +<title>A->M</title> +<path fill="none" stroke="blue" d="M156.07,-117.61C147.84,-116.01 139.03,-114.29 130.22,-112.58"/> +<polygon fill="blue" stroke="blue" points="131.06,-109.18 120.58,-110.7 129.72,-116.05 131.06,-109.18"/> +</g> +<!-- B --> +<g id="node3" class="node"> +<title>B</title> +<polygon fill="none" stroke="black" points="245.75,-90 154.75,-90 154.75,-54 245.75,-54 245.75,-90"/> +<text text-anchor="middle" x="200.25" y="-66.95" font-family="Times New Roman,serif" font-size="14.00">False: Store y</text> +</g> +<!-- B->M --> +<g id="edge4" class="edge"> +<title>B->M</title> +<path fill="none" stroke="blue" d="M154.54,-80.69C146.8,-82.2 138.59,-83.79 130.37,-85.39"/> +<polygon fill="blue" stroke="blue" points="129.83,-81.93 120.68,-87.28 131.16,-88.8 129.83,-81.93"/> +</g> +<!-- P --> +<g id="node4" class="node"> +<title>P</title> +<polygon fill="none" stroke="black" points="371.25,-90 281.75,-90 281.75,-54 371.25,-54 371.25,-90"/> +<text text-anchor="middle" x="326.5" y="-66.95" font-family="Times New Roman,serif" font-size="14.00">Phi(memory)</text> +</g> +<!-- P->A --> +<g id="edge1" class="edge"> +<title>P->A</title> +<path fill="none" stroke="blue" d="M283.09,-90.43C273.69,-94.52 263.62,-98.89 253.88,-103.13"/> +<polygon fill="blue" stroke="blue" points="252.64,-99.85 244.86,-107.05 255.43,-106.27 252.64,-99.85"/> +</g> +<!-- P->B --> +<g id="edge3" class="edge"> +<title>P->B</title> +<path fill="none" stroke="blue" d="M281.34,-72C273.66,-72 265.57,-72 257.61,-72"/> +<polygon fill="blue" stroke="blue" points="257.62,-68.5 247.62,-72 257.62,-75.5 257.62,-68.5"/> +</g> +<!-- Region --> +<g id="node5" class="node"> +<title>Region</title> +<polygon fill="none" stroke="black" points="227.75,-36 172.75,-36 172.75,0 227.75,0 227.75,-36"/> +<text text-anchor="middle" x="200.25" y="-12.95" font-family="Times New Roman,serif" font-size="14.00">Region</text> +</g> +<!-- P->Region --> +<g id="edge5" class="edge"> +<title>P->Region</title> +<path fill="none" stroke="black" stroke-dasharray="5,2" d="M283.09,-53.57C268.52,-47.24 252.34,-40.21 238.24,-34.08"/> +<polygon fill="black" stroke="black" points="239.88,-30.97 229.31,-30.2 237.09,-37.39 239.88,-30.97"/> +</g> +<!-- Return --> +<g id="node6" class="node"> +<title>Return</title> +<polygon fill="none" stroke="black" points="461.25,-90 407.25,-90 407.25,-54 461.25,-54 461.25,-90"/> +<text text-anchor="middle" x="434.25" y="-66.95" font-family="Times New Roman,serif" font-size="14.00">Return</text> +</g> +<!-- Return->P --> +<g id="edge6" class="edge"> +<title>Return->P</title> +<path fill="none" stroke="blue" d="M406.84,-72C399.52,-72 391.29,-72 382.97,-72"/> +<polygon fill="blue" stroke="blue" points="383.17,-68.5 373.17,-72 383.17,-75.5 383.17,-68.5"/> +</g> +</g> +</svg> diff --git a/docs/chapter10a/example2b.gv b/docs/chapter10a/example2b.gv new file mode 100644 index 00000000..3f3309e5 --- /dev/null +++ b/docs/chapter10a/example2b.gv @@ -0,0 +1,11 @@ +digraph Memory { + rankdir=RL; + node [shape=box]; + P [label="Phi(memory)"]; + S [label="Loop-body Store"]; + P -> Entry [color=blue]; + P -> S [color=blue,constraint=false]; // Backedge must not place Store before Phi. + S -> P [color=blue]; + P -> Loop [style=dashed]; Return -> P [color=blue]; + Return -> S [style=invis]; // Layout only: Phi, Store, Return from left to right. +} diff --git a/docs/chapter10a/example2b.svg b/docs/chapter10a/example2b.svg new file mode 100644 index 00000000..8fff33d4 --- /dev/null +++ b/docs/chapter10a/example2b.svg @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 12.2.0 (20241103.1931) + --> +<!-- Title: Memory Pages: 1 --> +<svg width="422pt" height="98pt" + viewBox="0.00 0.00 421.75 98.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 94)"> +<title>Memory</title> +<polygon fill="white" stroke="none" points="-4,4 -4,-94 417.75,-94 417.75,4 -4,4"/> +<!-- P --> +<g id="node1" class="node"> +<title>P</title> +<polygon fill="none" stroke="black" points="179.5,-63 90,-63 90,-27 179.5,-27 179.5,-63"/> +<text text-anchor="middle" x="134.75" y="-39.95" font-family="Times New Roman,serif" font-size="14.00">Phi(memory)</text> +</g> +<!-- S --> +<g id="node2" class="node"> +<title>S</title> +<polygon fill="none" stroke="black" points="323.75,-40 215.5,-40 215.5,-4 323.75,-4 323.75,-40"/> +<text text-anchor="middle" x="269.62" y="-16.95" font-family="Times New Roman,serif" font-size="14.00">Loop-body Store</text> +</g> +<!-- P->S --> +<g id="edge2" class="edge"> +<title>P->S</title> +<path fill="none" stroke="blue" d="M179.67,-43.79C187.52,-42.69 195.84,-41.37 204.12,-39.91"/> +<polygon fill="blue" stroke="blue" points="204.65,-43.37 213.86,-38.13 203.39,-36.49 204.65,-43.37"/> +</g> +<!-- Entry --> +<g id="node3" class="node"> +<title>Entry</title> +<polygon fill="none" stroke="black" points="54,-90 0,-90 0,-54 54,-54 54,-90"/> +<text text-anchor="middle" x="27" y="-66.95" font-family="Times New Roman,serif" font-size="14.00">Entry</text> +</g> +<!-- P->Entry --> +<g id="edge1" class="edge"> +<title>P->Entry</title> +<path fill="none" stroke="blue" d="M89.52,-56.29C81.45,-58.35 73.12,-60.48 65.28,-62.48"/> +<polygon fill="blue" stroke="blue" points="64.54,-59.06 55.72,-64.92 66.27,-65.84 64.54,-59.06"/> +</g> +<!-- Loop --> +<g id="node4" class="node"> +<title>Loop</title> +<polygon fill="none" stroke="black" points="54,-36 0,-36 0,0 54,0 54,-36"/> +<text text-anchor="middle" x="27" y="-12.95" font-family="Times New Roman,serif" font-size="14.00">Loop</text> +</g> +<!-- P->Loop --> +<g id="edge4" class="edge"> +<title>P->Loop</title> +<path fill="none" stroke="black" stroke-dasharray="5,2" d="M89.52,-33.71C81.45,-31.65 73.12,-29.52 65.28,-27.52"/> +<polygon fill="black" stroke="black" points="66.27,-24.16 55.72,-25.08 64.54,-30.94 66.27,-24.16"/> +</g> +<!-- S->P --> +<g id="edge3" class="edge"> +<title>S->P</title> +<path fill="none" stroke="blue" d="M215.35,-24.59C207.3,-25.84 198.98,-27.27 190.89,-28.79"/> +<polygon fill="blue" stroke="blue" points="190.29,-25.34 181.15,-30.71 191.64,-32.21 190.29,-25.34"/> +</g> +<!-- Return --> +<g id="node5" class="node"> +<title>Return</title> +<polygon fill="none" stroke="black" points="413.75,-63 359.75,-63 359.75,-27 413.75,-27 413.75,-63"/> +<text text-anchor="middle" x="386.75" y="-39.95" font-family="Times New Roman,serif" font-size="14.00">Return</text> +</g> +<!-- Return->P --> +<g id="edge5" class="edge"> +<title>Return->P</title> +<path fill="none" stroke="blue" d="M359.52,-47.07C348.49,-47.84 335.5,-48.62 323.75,-49 275.66,-50.55 263.6,-50.21 215.5,-49 207.59,-48.8 199.24,-48.49 191.04,-48.13"/> +<polygon fill="blue" stroke="blue" points="191.28,-44.64 181.12,-47.67 190.95,-51.63 191.28,-44.64"/> +</g> +<!-- Return->S --> +</g> +</svg> diff --git a/docs/chapter10a/example2c.gv b/docs/chapter10a/example2c.gv new file mode 100644 index 00000000..ef8a5fc8 --- /dev/null +++ b/docs/chapter10a/example2c.gv @@ -0,0 +1,9 @@ +digraph Memory { + rankdir=RL; + node [shape=box]; + M [label="Prior memory"]; + L [label="Load p.x"]; + S [label="Store p.x"]; + L -> M [color=blue]; S -> M [color=blue]; + S -> L [style=dashed,label="execute load first"]; +} diff --git a/docs/chapter10a/example2c.svg b/docs/chapter10a/example2c.svg new file mode 100644 index 00000000..c7b37912 --- /dev/null +++ b/docs/chapter10a/example2c.svg @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 12.2.0 (20241103.1931) + --> +<!-- Title: Memory Pages: 1 --> +<svg width="398pt" height="67pt" + viewBox="0.00 0.00 398.25 67.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 63)"> +<title>Memory</title> +<polygon fill="white" stroke="none" points="-4,4 -4,-63 394.25,-63 394.25,4 -4,4"/> +<!-- M --> +<g id="node1" class="node"> +<title>M</title> +<polygon fill="none" stroke="black" points="93.25,-59 0,-59 0,-23 93.25,-23 93.25,-59"/> +<text text-anchor="middle" x="46.62" y="-35.95" font-family="Times New Roman,serif" font-size="14.00">Prior memory</text> +</g> +<!-- L --> +<g id="node2" class="node"> +<title>L</title> +<polygon fill="none" stroke="black" points="195,-36 130.25,-36 130.25,0 195,0 195,-36"/> +<text text-anchor="middle" x="162.62" y="-12.95" font-family="Times New Roman,serif" font-size="14.00">Load p.x</text> +</g> +<!-- L->M --> +<g id="edge1" class="edge"> +<title>L->M</title> +<path fill="none" stroke="blue" d="M129.87,-24.41C122.08,-25.98 113.51,-27.71 104.92,-29.44"/> +<polygon fill="blue" stroke="blue" points="104.29,-26 95.18,-31.41 105.68,-32.86 104.29,-26"/> +</g> +<!-- S --> +<g id="node3" class="node"> +<title>S</title> +<polygon fill="none" stroke="black" points="390.25,-59 324.75,-59 324.75,-23 390.25,-23 390.25,-59"/> +<text text-anchor="middle" x="357.5" y="-35.95" font-family="Times New Roman,serif" font-size="14.00">Store p.x</text> +</g> +<!-- S->M --> +<g id="edge2" class="edge"> +<title>S->M</title> +<path fill="none" stroke="blue" d="M324.5,-47.66C318.62,-48.63 312.52,-49.48 306.75,-50 237.56,-56.21 157.62,-51.66 105.04,-47.07"/> +<polygon fill="blue" stroke="blue" points="105.37,-43.59 95.1,-46.17 104.74,-50.56 105.37,-43.59"/> +</g> +<!-- S->L --> +<g id="edge3" class="edge"> +<title>S->L</title> +<path fill="none" stroke="black" stroke-dasharray="5,2" d="M324.41,-35.96C318.54,-35.1 312.47,-34.24 306.75,-33.5 273.09,-29.14 234.87,-25.05 206.4,-22.16"/> +<polygon fill="black" stroke="black" points="207.03,-18.71 196.73,-21.19 206.33,-25.67 207.03,-18.71"/> +<text text-anchor="middle" x="259.88" y="-36.7" font-family="Times New Roman,serif" font-size="14.00">execute load first</text> +</g> +</g> +</svg> diff --git a/docs/chapter10a/lattice.gv b/docs/chapter10a/lattice.gv new file mode 100644 index 00000000..733a5287 --- /dev/null +++ b/docs/chapter10a/lattice.gv @@ -0,0 +1,108 @@ +digraph Lattice { + rankdir=TB; + newrank=true; + // Preserve the same-rank ordering edges through cluster layout. + remincross=false; + nodesep=0.22; + ranksep=0.65; + node [shape=ellipse, style=filled, fontname="DejaVu Sans", fontsize=13]; + edge [color="#555555", arrowsize=0.6]; + + // These arrows are lattice-order edges, not use/definition edges. + TOP [label="⊤", fillcolor=black, fontcolor=white]; + BOT [label="⊥", fillcolor=black, fontcolor=white]; + + subgraph cluster_control { + label="Control"; fontname="Arial"; color="#cccccc"; + node [fillcolor="#ffff00"]; + XCTRL [label="⊤:ctrl"]; + CTRL [label="⊥:ctrl"]; + XCTRL -> CTRL; + } + + subgraph cluster_integer { + label="Integer"; fontname="Arial"; color="#cccccc"; + node [fillcolor="#add8e6"]; + INT_TOP [label="⊤:int"]; + INT_BOT [label="⊥:int"]; + INT_NEG [label="… −2, −1"]; + INT_ZERO [label="0"]; + INT_POS [label="1, 2, …"]; + {rank=same; INT_NEG; INT_ZERO; INT_POS;} + INT_NEG -> INT_ZERO -> INT_POS [style=invis]; + INT_TOP -> {INT_NEG INT_ZERO INT_POS}; + {INT_NEG INT_ZERO INT_POS} -> INT_BOT; + } + + subgraph cluster_pointer { + label="Pointer = nullability × struct type"; + fontname="Arial"; color="#cccccc"; + node [fillcolor="#008000", fontcolor=white]; + PTR_TOP [label="⊤:ptr\n[*⊤:struct]"]; + PTR_BOT [label="⊥:ptr\n[*⊥:struct?]"]; + PTR_NULL [label="null"]; + PTR_ANY [label="*⊥:struct\n[ptr any struct]"]; + PTR_PERSON [label="*Person"]; + PTR_S1 [label="*s1"]; + PTR_MORE [label="*s2, …"]; + PTR_PERSON_NIL [label="*Person?"]; + PTR_S1_NIL [label="*s1?"]; + PTR_MORE_NIL [label="*s2?, …"]; + {rank=same; PTR_PERSON; PTR_S1; PTR_MORE; PTR_NULL;} + {rank=same; PTR_ANY; PTR_PERSON_NIL; PTR_S1_NIL; PTR_MORE_NIL;} + PTR_PERSON -> PTR_S1 -> PTR_MORE -> PTR_NULL [style=invis]; + PTR_ANY -> PTR_PERSON_NIL -> PTR_S1_NIL -> PTR_MORE_NIL [style=invis]; + PTR_TOP -> {PTR_PERSON PTR_S1 PTR_MORE PTR_NULL}; + {PTR_PERSON PTR_S1 PTR_MORE} -> PTR_ANY; + PTR_PERSON -> PTR_PERSON_NIL; + PTR_S1 -> PTR_S1_NIL; + PTR_MORE -> PTR_MORE_NIL; + PTR_NULL -> {PTR_PERSON_NIL PTR_S1_NIL PTR_MORE_NIL}; + {PTR_ANY PTR_PERSON_NIL PTR_S1_NIL PTR_MORE_NIL} -> PTR_BOT; + } + + subgraph cluster_struct { + label="Struct\nnamed collection of named scalar types"; + fontname="Arial"; color="#cccccc"; + node [fillcolor="#90ee90"]; + STRUCT_TOP [label="⊤:struct\n[struct top]"]; + STRUCT_BOT [label="⊥:struct\n[struct bottom]"]; + STRUCT_PERSON [label="Person\nage: int", shape=box]; + STRUCT_S1 [label="s1\nx: int", shape=box]; + STRUCT_S2 [label="s2\nx: int, y: *s1", shape=box]; + STRUCT_MORE [label="…", shape=box]; + {rank=same; STRUCT_PERSON; STRUCT_S1; STRUCT_S2; STRUCT_MORE;} + STRUCT_PERSON -> STRUCT_S1 -> STRUCT_S2 -> STRUCT_MORE [style=invis]; + STRUCT_TOP -> {STRUCT_PERSON STRUCT_S1 STRUCT_S2 STRUCT_MORE}; + {STRUCT_PERSON STRUCT_S1 STRUCT_S2 STRUCT_MORE} -> STRUCT_BOT; + } + + subgraph cluster_memory { + label="Memory"; fontname="Arial"; color="#cccccc"; + node [fillcolor="#00008b", fontcolor=white]; + MEM_TOP [label="⊤:mem"]; + MEM_BOT [label="⊥:mem"]; + MEM_TOP -> MEM_BOT; + } + + subgraph cluster_tuple { + label="Tuple\ncollection of types"; + fontname="Arial"; color="#cccccc"; + node [fillcolor="#ffc0cb"]; + TUPLE_TOP [label="⊤:tuple"]; + TUPLE_BOT [label="⊥:tuple"]; + TUPLE1 [label="(type)", shape=box]; + TUPLE2 [label="(type, type)", shape=box]; + TUPLEN [label="(type, …)", shape=box]; + {rank=same; TUPLE1; TUPLE2; TUPLEN;} + TUPLE1 -> TUPLE2 -> TUPLEN [style=invis]; + TUPLE_TOP -> {TUPLE1 TUPLE2 TUPLEN}; + {TUPLE1 TUPLE2 TUPLEN} -> TUPLE_BOT; + } + + TOP -> {XCTRL INT_TOP PTR_TOP STRUCT_TOP MEM_TOP TUPLE_TOP}; + {CTRL INT_BOT PTR_BOT STRUCT_BOT MEM_BOT TUPLE_BOT} -> BOT; + {rank=same; XCTRL; INT_TOP; PTR_TOP; STRUCT_TOP; MEM_TOP; TUPLE_TOP;} + {rank=same; CTRL; INT_BOT; PTR_BOT; STRUCT_BOT; MEM_BOT; TUPLE_BOT;} + XCTRL -> INT_TOP -> PTR_TOP -> STRUCT_TOP -> MEM_TOP -> TUPLE_TOP [style=invis]; +} diff --git a/docs/chapter10a/lattice.svg b/docs/chapter10a/lattice.svg new file mode 100644 index 00000000..24c0de8c --- /dev/null +++ b/docs/chapter10a/lattice.svg @@ -0,0 +1,570 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 12.2.0 (20241103.1931) + --> +<!-- Title: Lattice Pages: 1 --> +<svg width="1647pt" height="555pt" + viewBox="0.00 0.00 1647.00 555.43" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 551.43)"> +<title>Lattice</title> +<polygon fill="white" stroke="none" points="-4,4 -4,-551.43 1643,-551.43 1643,4 -4,4"/> +<g id="clust1" class="cluster"> +<title>cluster_control</title> +<polygon fill="none" stroke="#cccccc" points="8,-85.99 8,-476.69 98,-476.69 98,-85.99 8,-85.99"/> +<text text-anchor="middle" x="53" y="-459.39" font-family="Arial" font-size="14.00">Control</text> +</g> +<g id="clust2" class="cluster"> +<title>cluster_integer</title> +<polygon fill="none" stroke="#cccccc" points="106,-85.99 106,-476.69 404,-476.69 404,-85.99 106,-85.99"/> +<text text-anchor="middle" x="255" y="-459.39" font-family="Arial" font-size="14.00">Integer</text> +</g> +<g id="clust6" class="cluster"> +<title>cluster_pointer</title> +<polygon fill="none" stroke="#cccccc" points="412,-75 412,-487.68 893,-487.68 893,-75 412,-75"/> +<text text-anchor="middle" x="652.5" y="-470.38" font-family="Arial" font-size="14.00">Pointer = nullability × struct type</text> +</g> +<g id="clust13" class="cluster"> +<title>cluster_struct</title> +<polygon fill="none" stroke="#cccccc" points="901,-75 901,-503.43 1233,-503.43 1233,-75 901,-75"/> +<text text-anchor="middle" x="1067" y="-486.13" font-family="Arial" font-size="14.00">Struct</text> +<text text-anchor="middle" x="1067" y="-470.38" font-family="Arial" font-size="14.00">named collection of named scalar types</text> +</g> +<g id="clust17" class="cluster"> +<title>cluster_memory</title> +<polygon fill="none" stroke="#cccccc" points="1241,-85.99 1241,-476.69 1347,-476.69 1347,-85.99 1241,-85.99"/> +<text text-anchor="middle" x="1294" y="-459.39" font-family="Arial" font-size="14.00">Memory</text> +</g> +<g id="clust18" class="cluster"> +<title>cluster_tuple</title> +<polygon fill="none" stroke="#cccccc" points="1355,-85.99 1355,-492.44 1631,-492.44 1631,-85.99 1355,-85.99"/> +<text text-anchor="middle" x="1493" y="-475.14" font-family="Arial" font-size="14.00">Tuple</text> +<text text-anchor="middle" x="1493" y="-459.39" font-family="Arial" font-size="14.00">collection of types</text> +</g> +<!-- TOP --> +<g id="node1" class="node"> +<title>TOP</title> +<ellipse fill="black" stroke="black" cx="845" cy="-529.43" rx="27" ry="18"/> +<text text-anchor="middle" x="845" y="-525.33" font-family="DejaVu Sans" font-size="13.00" fill="white">⊤</text> +</g> +<!-- XCTRL --> +<g id="node3" class="node"> +<title>XCTRL</title> +<ellipse fill="#ffff00" stroke="black" cx="53" cy="-426.94" rx="36.51" ry="18"/> +<text text-anchor="middle" x="53" y="-422.84" font-family="DejaVu Sans" font-size="13.00">⊤:ctrl</text> +</g> +<!-- TOP->XCTRL --> +<g id="edge53" class="edge"> +<title>TOP->XCTRL</title> +<path fill="none" stroke="#555555" d="M817.66,-528.22C687.28,-527.11 133.71,-521.35 102,-503.43 82.18,-492.23 69.06,-469.34 61.42,-451.68"/> +<polygon fill="#555555" stroke="#555555" points="63.46,-451.12 59.24,-446.37 59.57,-452.72 63.46,-451.12"/> +</g> +<!-- INT_TOP --> +<g id="node5" class="node"> +<title>INT_TOP</title> +<ellipse fill="#add8e6" stroke="black" cx="353" cy="-426.94" rx="33.44" ry="18"/> +<text text-anchor="middle" x="353" y="-422.84" font-family="DejaVu Sans" font-size="13.00">⊤:int</text> +</g> +<!-- TOP->INT_TOP --> +<g id="edge54" class="edge"> +<title>TOP->INT_TOP</title> +<path fill="none" stroke="#555555" d="M817.72,-528.22C727.72,-527.33 443.86,-522.98 408,-503.43 387.26,-492.12 372.32,-469.24 363.29,-451.62"/> +<polygon fill="#555555" stroke="#555555" points="365.2,-450.74 360.67,-446.28 361.42,-452.59 365.2,-450.74"/> +</g> +<!-- PTR_TOP --> +<g id="node10" class="node"> +<title>PTR_TOP</title> +<ellipse fill="#008000" stroke="black" cx="685" cy="-426.94" rx="61.7" ry="28.99"/> +<text text-anchor="middle" x="685" y="-431.09" font-family="DejaVu Sans" font-size="13.00" fill="white">⊤:ptr</text> +<text text-anchor="middle" x="685" y="-414.59" font-family="DejaVu Sans" font-size="13.00" fill="white">[*⊤:struct]</text> +</g> +<!-- TOP->PTR_TOP --> +<g id="edge55" class="edge"> +<title>TOP->PTR_TOP</title> +<path fill="none" stroke="#555555" d="M825.7,-516.31C801.54,-501.14 759.48,-474.72 727.62,-454.71"/> +<polygon fill="#555555" stroke="#555555" points="728.87,-453.01 722.67,-451.6 726.64,-456.57 728.87,-453.01"/> +</g> +<!-- STRUCT_TOP --> +<g id="node20" class="node"> +<title>STRUCT_TOP</title> +<ellipse fill="#90ee90" stroke="black" cx="996" cy="-426.94" rx="64.35" ry="28.99"/> +<text text-anchor="middle" x="996" y="-431.09" font-family="DejaVu Sans" font-size="13.00">⊤:struct</text> +<text text-anchor="middle" x="996" y="-414.59" font-family="DejaVu Sans" font-size="13.00">[struct top]</text> +</g> +<!-- TOP->STRUCT_TOP --> +<g id="edge56" class="edge"> +<title>TOP->STRUCT_TOP</title> +<path fill="none" stroke="#555555" d="M867.28,-518.93C876.68,-514.61 887.61,-509.19 897,-503.43 919.21,-489.82 942.45,-472.29 960.83,-457.55"/> +<polygon fill="#555555" stroke="#555555" points="961.85,-459.43 965.2,-454.02 959.21,-456.16 961.85,-459.43"/> +</g> +<!-- MEM_TOP --> +<g id="node26" class="node"> +<title>MEM_TOP</title> +<ellipse fill="#00008b" stroke="black" cx="1294" cy="-426.94" rx="44.7" ry="18"/> +<text text-anchor="middle" x="1294" y="-422.84" font-family="DejaVu Sans" font-size="13.00" fill="white">⊤:mem</text> +</g> +<!-- TOP->MEM_TOP --> +<g id="edge57" class="edge"> +<title>TOP->MEM_TOP</title> +<path fill="none" stroke="#555555" d="M872.35,-528C956.06,-526.49 1205.1,-520.6 1237,-503.43 1258.25,-491.99 1273.89,-468.81 1283.35,-451.14"/> +<polygon fill="#555555" stroke="#555555" points="1284.99,-452.55 1285.86,-446.25 1281.25,-450.62 1284.99,-452.55"/> +</g> +<!-- TUPLE_TOP --> +<g id="node28" class="node"> +<title>TUPLE_TOP</title> +<ellipse fill="#ffc0cb" stroke="black" cx="1408" cy="-426.94" rx="44.7" ry="18"/> +<text text-anchor="middle" x="1408" y="-422.84" font-family="DejaVu Sans" font-size="13.00">⊤:tuple</text> +</g> +<!-- TOP->TUPLE_TOP --> +<g id="edge58" class="edge"> +<title>TOP->TUPLE_TOP</title> +<path fill="none" stroke="#555555" d="M872.34,-528.42C971.49,-528.22 1308.63,-525.85 1351,-503.43 1372.33,-492.14 1387.95,-468.94 1397.39,-451.22"/> +<polygon fill="#555555" stroke="#555555" points="1399.04,-452.62 1399.9,-446.32 1395.3,-450.7 1399.04,-452.62"/> +</g> +<!-- BOT --> +<g id="node2" class="node"> +<title>BOT</title> +<ellipse fill="black" stroke="black" cx="884" cy="-18" rx="27" ry="18"/> +<text text-anchor="middle" x="884" y="-13.9" font-family="DejaVu Sans" font-size="13.00" fill="white">⊥</text> +</g> +<!-- CTRL --> +<g id="node4" class="node"> +<title>CTRL</title> +<ellipse fill="#ffff00" stroke="black" cx="53" cy="-111.99" rx="36.51" ry="18"/> +<text text-anchor="middle" x="53" y="-107.89" font-family="DejaVu Sans" font-size="13.00">⊥:ctrl</text> +</g> +<!-- XCTRL->CTRL --> +<g id="edge1" class="edge"> +<title>XCTRL->CTRL</title> +<path fill="none" stroke="#555555" d="M53,-408.67C53,-356.2 53,-198.53 53,-137.49"/> +<polygon fill="#555555" stroke="#555555" points="55.1,-137.72 53,-131.72 50.9,-137.72 55.1,-137.72"/> +</g> +<!-- XCTRL->INT_TOP --> +<!-- CTRL->BOT --> +<g id="edge59" class="edge"> +<title>CTRL->BOT</title> +<path fill="none" stroke="#555555" d="M69.29,-95.39C78.22,-87.83 89.91,-79.45 102,-75 242.26,-23.4 717.31,-19.12 849.57,-18.95"/> +<polygon fill="#555555" stroke="#555555" points="849.19,-21.05 855.19,-18.94 849.18,-16.85 849.19,-21.05"/> +</g> +<!-- INT_NEG --> +<g id="node7" class="node"> +<title>INT_NEG</title> +<ellipse fill="#add8e6" stroke="black" cx="169" cy="-216.97" rx="55.45" ry="18"/> +<text text-anchor="middle" x="169" y="-212.87" font-family="DejaVu Sans" font-size="13.00">… −2, −1</text> +</g> +<!-- INT_TOP->INT_NEG --> +<g id="edge4" class="edge"> +<title>INT_TOP->INT_NEG</title> +<path fill="none" stroke="#555555" d="M339.17,-410.31C306.78,-373.7 225.91,-282.29 188.52,-240.04"/> +<polygon fill="#555555" stroke="#555555" points="190.31,-238.89 184.76,-235.78 187.16,-241.67 190.31,-238.89"/> +</g> +<!-- INT_ZERO --> +<g id="node8" class="node"> +<title>INT_ZERO</title> +<ellipse fill="#add8e6" stroke="black" cx="267" cy="-216.97" rx="27" ry="18"/> +<text text-anchor="middle" x="267" y="-212.87" font-family="DejaVu Sans" font-size="13.00">0</text> +</g> +<!-- INT_TOP->INT_ZERO --> +<g id="edge5" class="edge"> +<title>INT_TOP->INT_ZERO</title> +<path fill="none" stroke="#555555" d="M345.96,-408.92C330.64,-371.87 294.24,-283.85 276.74,-241.52"/> +<polygon fill="#555555" stroke="#555555" points="278.71,-240.79 274.47,-236.05 274.83,-242.39 278.71,-240.79"/> +</g> +<!-- INT_POS --> +<g id="node9" class="node"> +<title>INT_POS</title> +<ellipse fill="#add8e6" stroke="black" cx="353" cy="-216.97" rx="43.16" ry="18"/> +<text text-anchor="middle" x="353" y="-212.87" font-family="DejaVu Sans" font-size="13.00">1, 2, …</text> +</g> +<!-- INT_TOP->INT_POS --> +<g id="edge6" class="edge"> +<title>INT_TOP->INT_POS</title> +<path fill="none" stroke="#555555" d="M353,-408.57C353,-371.71 353,-285.59 353,-242.89"/> +<polygon fill="#555555" stroke="#555555" points="355.1,-242.96 353,-236.96 350.9,-242.96 355.1,-242.96"/> +</g> +<!-- INT_TOP->PTR_TOP --> +<!-- INT_BOT --> +<g id="node6" class="node"> +<title>INT_BOT</title> +<ellipse fill="#add8e6" stroke="black" cx="310" cy="-111.99" rx="33.44" ry="18"/> +<text text-anchor="middle" x="310" y="-107.89" font-family="DejaVu Sans" font-size="13.00">⊥:int</text> +</g> +<!-- INT_BOT->BOT --> +<g id="edge60" class="edge"> +<title>INT_BOT->BOT</title> +<path fill="none" stroke="#555555" d="M335.28,-99.84C354.78,-91.7 382.61,-81.07 408,-75 570.29,-36.22 770.4,-23.83 849.5,-20.29"/> +<polygon fill="#555555" stroke="#555555" points="849.36,-22.39 855.26,-20.04 849.17,-18.2 849.36,-22.39"/> +</g> +<!-- INT_NEG->INT_BOT --> +<g id="edge7" class="edge"> +<title>INT_NEG->INT_BOT</title> +<path fill="none" stroke="#555555" d="M190.72,-200.11C216.15,-181.54 258.17,-150.85 284.95,-131.29"/> +<polygon fill="#555555" stroke="#555555" points="285.92,-133.18 289.52,-127.95 283.44,-129.79 285.92,-133.18"/> +</g> +<!-- INT_NEG->INT_ZERO --> +<!-- INT_ZERO->INT_BOT --> +<g id="edge8" class="edge"> +<title>INT_ZERO->INT_BOT</title> +<path fill="none" stroke="#555555" d="M273.91,-199.43C281.01,-182.41 292.14,-155.76 300.16,-136.55"/> +<polygon fill="#555555" stroke="#555555" points="302.01,-137.58 302.38,-131.24 298.13,-135.96 302.01,-137.58"/> +</g> +<!-- INT_ZERO->INT_POS --> +<!-- INT_POS->INT_BOT --> +<g id="edge9" class="edge"> +<title>INT_POS->INT_BOT</title> +<path fill="none" stroke="#555555" d="M345.9,-198.97C338.83,-182.05 327.93,-155.94 319.99,-136.93"/> +<polygon fill="#555555" stroke="#555555" points="321.94,-136.15 317.69,-131.42 318.07,-137.76 321.94,-136.15"/> +</g> +<!-- PTR_NULL --> +<g id="node12" class="node"> +<title>PTR_NULL</title> +<ellipse fill="#008000" stroke="black" cx="808" cy="-321.96" rx="66.47" ry="28.99"/> +<text text-anchor="middle" x="808" y="-326.11" font-family="DejaVu Sans" font-size="13.00" fill="white">null</text> +<text text-anchor="middle" x="808" y="-309.61" font-family="DejaVu Sans" font-size="13.00" fill="white">[*⊤:struct?]</text> +</g> +<!-- PTR_TOP->PTR_NULL --> +<g id="edge16" class="edge"> +<title>PTR_TOP->PTR_NULL</title> +<path fill="none" stroke="#555555" d="M714.46,-401.27C731.85,-386.71 753.98,-368.18 772.35,-352.8"/> +<polygon fill="#555555" stroke="#555555" points="773.51,-354.57 776.76,-349.11 770.82,-351.35 773.51,-354.57"/> +</g> +<!-- PTR_PERSON --> +<g id="node14" class="node"> +<title>PTR_PERSON</title> +<ellipse fill="#008000" stroke="black" cx="512" cy="-321.96" rx="45.72" ry="18"/> +<text text-anchor="middle" x="512" y="-317.86" font-family="DejaVu Sans" font-size="13.00" fill="white">*Person</text> +</g> +<!-- PTR_TOP->PTR_PERSON --> +<g id="edge17" class="edge"> +<title>PTR_TOP->PTR_PERSON</title> +<path fill="none" stroke="#555555" d="M647.46,-403.59C616.14,-384.95 572.05,-358.7 542.78,-341.28"/> +<polygon fill="#555555" stroke="#555555" points="543.94,-339.52 537.71,-338.26 541.79,-343.13 543.94,-339.52"/> +</g> +<!-- PTR_S1 --> +<g id="node15" class="node"> +<title>PTR_S1</title> +<ellipse fill="#008000" stroke="black" cx="601" cy="-321.96" rx="27" ry="18"/> +<text text-anchor="middle" x="601" y="-317.86" font-family="DejaVu Sans" font-size="13.00" fill="white">*s1</text> +</g> +<!-- PTR_TOP->PTR_S1 --> +<g id="edge18" class="edge"> +<title>PTR_TOP->PTR_S1</title> +<path fill="none" stroke="#555555" d="M663.37,-399.42C649.25,-382.12 631.09,-359.85 618.01,-343.81"/> +<polygon fill="#555555" stroke="#555555" points="619.81,-342.7 614.39,-339.38 616.56,-345.36 619.81,-342.7"/> +</g> +<!-- PTR_MORE --> +<g id="node16" class="node"> +<title>PTR_MORE</title> +<ellipse fill="#008000" stroke="black" cx="685" cy="-321.96" rx="40.6" ry="18"/> +<text text-anchor="middle" x="685" y="-317.86" font-family="DejaVu Sans" font-size="13.00" fill="white">*s2, …</text> +</g> +<!-- PTR_TOP->PTR_MORE --> +<g id="edge19" class="edge"> +<title>PTR_TOP->PTR_MORE</title> +<path fill="none" stroke="#555555" d="M685,-397.53C685,-381.91 685,-362.73 685,-347.73"/> +<polygon fill="#555555" stroke="#555555" points="687.1,-347.82 685,-341.82 682.9,-347.82 687.1,-347.82"/> +</g> +<!-- PTR_TOP->STRUCT_TOP --> +<!-- PTR_BOT --> +<g id="node11" class="node"> +<title>PTR_BOT</title> +<ellipse fill="#008000" stroke="black" cx="748" cy="-111.99" rx="66.47" ry="28.99"/> +<text text-anchor="middle" x="748" y="-116.14" font-family="DejaVu Sans" font-size="13.00" fill="white">⊥:ptr</text> +<text text-anchor="middle" x="748" y="-99.64" font-family="DejaVu Sans" font-size="13.00" fill="white">[*⊥:struct?]</text> +</g> +<!-- PTR_BOT->BOT --> +<g id="edge61" class="edge"> +<title>PTR_BOT->BOT</title> +<path fill="none" stroke="#555555" d="M783.38,-87.06C807.3,-70.88 838.29,-49.92 859.52,-35.56"/> +<polygon fill="#555555" stroke="#555555" points="860.46,-37.46 864.25,-32.36 858.1,-33.98 860.46,-37.46"/> +</g> +<!-- PTR_PERSON_NIL --> +<g id="node17" class="node"> +<title>PTR_PERSON_NIL</title> +<ellipse fill="#008000" stroke="black" cx="651" cy="-216.97" rx="50.33" ry="18"/> +<text text-anchor="middle" x="651" y="-212.87" font-family="DejaVu Sans" font-size="13.00" fill="white">*Person?</text> +</g> +<!-- PTR_NULL->PTR_PERSON_NIL --> +<g id="edge26" class="edge"> +<title>PTR_NULL->PTR_PERSON_NIL</title> +<path fill="none" stroke="#555555" d="M771.98,-297.33C744.16,-279.08 706.14,-254.14 680.2,-237.12"/> +<polygon fill="#555555" stroke="#555555" points="681.63,-235.56 675.47,-234.02 679.33,-239.07 681.63,-235.56"/> +</g> +<!-- PTR_S1_NIL --> +<g id="node18" class="node"> +<title>PTR_S1_NIL</title> +<ellipse fill="#008000" stroke="black" cx="748" cy="-216.97" rx="30.37" ry="18"/> +<text text-anchor="middle" x="748" y="-212.87" font-family="DejaVu Sans" font-size="13.00" fill="white">*s1?</text> +</g> +<!-- PTR_NULL->PTR_S1_NIL --> +<g id="edge27" class="edge"> +<title>PTR_NULL->PTR_S1_NIL</title> +<path fill="none" stroke="#555555" d="M791.92,-293.36C782.32,-276.89 770.28,-256.22 761.24,-240.69"/> +<polygon fill="#555555" stroke="#555555" points="763.16,-239.82 758.32,-235.69 759.53,-241.93 763.16,-239.82"/> +</g> +<!-- PTR_MORE_NIL --> +<g id="node19" class="node"> +<title>PTR_MORE_NIL</title> +<ellipse fill="#008000" stroke="black" cx="840" cy="-216.97" rx="45.21" ry="18"/> +<text text-anchor="middle" x="840" y="-212.87" font-family="DejaVu Sans" font-size="13.00" fill="white">*s2?, …</text> +</g> +<!-- PTR_NULL->PTR_MORE_NIL --> +<g id="edge28" class="edge"> +<title>PTR_NULL->PTR_MORE_NIL</title> +<path fill="none" stroke="#555555" d="M816.74,-292.82C821.67,-276.95 827.77,-257.33 832.48,-242.16"/> +<polygon fill="#555555" stroke="#555555" points="834.45,-242.91 834.23,-236.56 830.44,-241.66 834.45,-242.91"/> +</g> +<!-- PTR_ANY --> +<g id="node13" class="node"> +<title>PTR_ANY</title> +<ellipse fill="#008000" stroke="black" cx="502" cy="-216.97" rx="82.38" ry="28.99"/> +<text text-anchor="middle" x="502" y="-221.12" font-family="DejaVu Sans" font-size="13.00" fill="white">*⊥:struct</text> +<text text-anchor="middle" x="502" y="-204.62" font-family="DejaVu Sans" font-size="13.00" fill="white">[ptr any struct]</text> +</g> +<!-- PTR_ANY->PTR_BOT --> +<g id="edge29" class="edge"> +<title>PTR_ANY->PTR_BOT</title> +<path fill="none" stroke="#555555" d="M554.17,-194.13C595.32,-176.91 652.51,-152.97 694.1,-135.56"/> +<polygon fill="#555555" stroke="#555555" points="694.78,-137.55 699.5,-133.29 693.15,-133.67 694.78,-137.55"/> +</g> +<!-- PTR_ANY->PTR_PERSON_NIL --> +<!-- PTR_PERSON->PTR_ANY --> +<g id="edge20" class="edge"> +<title>PTR_PERSON->PTR_ANY</title> +<path fill="none" stroke="#555555" d="M510.3,-303.49C508.98,-289.83 507.09,-270.39 505.45,-253.5"/> +<polygon fill="#555555" stroke="#555555" points="507.58,-253.71 504.91,-247.94 503.4,-254.12 507.58,-253.71"/> +</g> +<!-- PTR_PERSON->PTR_S1 --> +<!-- PTR_PERSON->PTR_PERSON_NIL --> +<g id="edge23" class="edge"> +<title>PTR_PERSON->PTR_PERSON_NIL</title> +<path fill="none" stroke="#555555" d="M532.81,-305.54C557.17,-287.49 597.57,-257.56 624.25,-237.8"/> +<polygon fill="#555555" stroke="#555555" points="625.26,-239.66 628.83,-234.4 622.76,-236.28 625.26,-239.66"/> +</g> +<!-- PTR_S1->PTR_ANY --> +<g id="edge21" class="edge"> +<title>PTR_S1->PTR_ANY</title> +<path fill="none" stroke="#555555" d="M586.82,-306.2C572.74,-291.56 550.72,-268.65 532.64,-249.85"/> +<polygon fill="#555555" stroke="#555555" points="534.38,-248.62 528.7,-245.75 531.35,-251.53 534.38,-248.62"/> +</g> +<!-- PTR_S1->PTR_MORE --> +<!-- PTR_S1->PTR_S1_NIL --> +<g id="edge24" class="edge"> +<title>PTR_S1->PTR_S1_NIL</title> +<path fill="none" stroke="#555555" d="M617.46,-307.22C623.23,-302.56 629.81,-297.42 636,-292.97 667.33,-270.44 677.09,-267.67 709,-245.97 713.75,-242.74 718.77,-239.22 723.56,-235.8"/> +<polygon fill="#555555" stroke="#555555" points="724.61,-237.64 728.26,-232.43 722.16,-234.22 724.61,-237.64"/> +</g> +<!-- PTR_MORE->PTR_NULL --> +<!-- PTR_MORE->PTR_ANY --> +<g id="edge22" class="edge"> +<title>PTR_MORE->PTR_ANY</title> +<path fill="none" stroke="#555555" d="M660.85,-307.1C652.94,-302.58 644.1,-297.55 636,-292.97 608.08,-277.18 576.81,-259.67 551.5,-245.54"/> +<polygon fill="#555555" stroke="#555555" points="552.79,-243.85 546.52,-242.76 550.74,-247.52 552.79,-243.85"/> +</g> +<!-- PTR_MORE->PTR_MORE_NIL --> +<g id="edge25" class="edge"> +<title>PTR_MORE->PTR_MORE_NIL</title> +<path fill="none" stroke="#555555" d="M706.88,-306.42C734.22,-288.25 781.1,-257.11 811.3,-237.04"/> +<polygon fill="#555555" stroke="#555555" points="812.38,-238.85 816.22,-233.78 810.06,-235.35 812.38,-238.85"/> +</g> +<!-- PTR_PERSON_NIL->PTR_BOT --> +<g id="edge30" class="edge"> +<title>PTR_PERSON_NIL->PTR_BOT</title> +<path fill="none" stroke="#555555" d="M666.58,-199.43C680.58,-184.56 701.51,-162.35 718.65,-144.16"/> +<polygon fill="#555555" stroke="#555555" points="719.75,-146.05 722.33,-140.24 716.69,-143.17 719.75,-146.05"/> +</g> +<!-- PTR_PERSON_NIL->PTR_S1_NIL --> +<!-- PTR_S1_NIL->PTR_BOT --> +<g id="edge31" class="edge"> +<title>PTR_S1_NIL->PTR_BOT</title> +<path fill="none" stroke="#555555" d="M748,-198.5C748,-184.97 748,-165.76 748,-148.97"/> +<polygon fill="#555555" stroke="#555555" points="750.1,-148.97 748,-142.97 745.9,-148.97 750.1,-148.97"/> +</g> +<!-- PTR_S1_NIL->PTR_MORE_NIL --> +<!-- PTR_MORE_NIL->PTR_BOT --> +<g id="edge32" class="edge"> +<title>PTR_MORE_NIL->PTR_BOT</title> +<path fill="none" stroke="#555555" d="M825.62,-199.88C812.53,-185.22 792.8,-163.14 776.51,-144.9"/> +<polygon fill="#555555" stroke="#555555" points="778.14,-143.58 772.58,-140.51 775.01,-146.38 778.14,-143.58"/> +</g> +<!-- STRUCT_PERSON --> +<g id="node22" class="node"> +<title>STRUCT_PERSON</title> +<polygon fill="#90ee90" stroke="black" points="976.5,-237.47 909.5,-237.47 909.5,-196.47 976.5,-196.47 976.5,-237.47"/> +<text text-anchor="middle" x="943" y="-221.12" font-family="DejaVu Sans" font-size="13.00">Person</text> +<text text-anchor="middle" x="943" y="-204.62" font-family="DejaVu Sans" font-size="13.00">age: int</text> +</g> +<!-- STRUCT_TOP->STRUCT_PERSON --> +<g id="edge36" class="edge"> +<title>STRUCT_TOP->STRUCT_PERSON</title> +<path fill="none" stroke="#555555" d="M988.85,-397.89C978.61,-357.71 959.89,-284.26 949.83,-244.77"/> +<polygon fill="#555555" stroke="#555555" points="951.95,-244.58 948.43,-239.28 947.88,-245.61 951.95,-244.58"/> +</g> +<!-- STRUCT_S1 --> +<g id="node23" class="node"> +<title>STRUCT_S1</title> +<polygon fill="#90ee90" stroke="black" points="1047,-237.47 993,-237.47 993,-196.47 1047,-196.47 1047,-237.47"/> +<text text-anchor="middle" x="1020" y="-221.12" font-family="DejaVu Sans" font-size="13.00">s1</text> +<text text-anchor="middle" x="1020" y="-204.62" font-family="DejaVu Sans" font-size="13.00">x: int</text> +</g> +<!-- STRUCT_TOP->STRUCT_S1 --> +<g id="edge37" class="edge"> +<title>STRUCT_TOP->STRUCT_S1</title> +<path fill="none" stroke="#555555" d="M999.24,-397.89C1003.85,-357.88 1012.28,-284.87 1016.85,-245.27"/> +<polygon fill="#555555" stroke="#555555" points="1018.94,-245.52 1017.54,-239.32 1014.76,-245.04 1018.94,-245.52"/> +</g> +<!-- STRUCT_S2 --> +<g id="node24" class="node"> +<title>STRUCT_S2</title> +<polygon fill="#90ee90" stroke="black" points="1154.88,-237.47 1063.12,-237.47 1063.12,-196.47 1154.88,-196.47 1154.88,-237.47"/> +<text text-anchor="middle" x="1109" y="-221.12" font-family="DejaVu Sans" font-size="13.00">s2</text> +<text text-anchor="middle" x="1109" y="-204.62" font-family="DejaVu Sans" font-size="13.00">x: int, y: int</text> +</g> +<!-- STRUCT_TOP->STRUCT_S2 --> +<g id="edge38" class="edge"> +<title>STRUCT_TOP->STRUCT_S2</title> +<path fill="none" stroke="#555555" d="M1011.01,-398.32C1032.86,-358.11 1073.16,-283.94 1094.64,-244.41"/> +<polygon fill="#555555" stroke="#555555" points="1096.43,-245.51 1097.45,-239.24 1092.74,-243.51 1096.43,-245.51"/> +</g> +<!-- STRUCT_MORE --> +<g id="node25" class="node"> +<title>STRUCT_MORE</title> +<polygon fill="#90ee90" stroke="black" points="1225,-234.97 1171,-234.97 1171,-198.97 1225,-198.97 1225,-234.97"/> +<text text-anchor="middle" x="1198" y="-212.87" font-family="DejaVu Sans" font-size="13.00">…</text> +</g> +<!-- STRUCT_TOP->STRUCT_MORE --> +<g id="edge39" class="edge"> +<title>STRUCT_TOP->STRUCT_MORE</title> +<path fill="none" stroke="#555555" d="M1021.18,-400.02C1061.16,-358.86 1138.49,-279.24 1176.23,-240.39"/> +<polygon fill="#555555" stroke="#555555" points="1177.33,-242.26 1180.01,-236.5 1174.32,-239.34 1177.33,-242.26"/> +</g> +<!-- STRUCT_TOP->MEM_TOP --> +<!-- STRUCT_BOT --> +<g id="node21" class="node"> +<title>STRUCT_BOT</title> +<ellipse fill="#90ee90" stroke="black" cx="1020" cy="-111.99" rx="82.91" ry="28.99"/> +<text text-anchor="middle" x="1020" y="-116.14" font-family="DejaVu Sans" font-size="13.00">⊥:struct</text> +<text text-anchor="middle" x="1020" y="-99.64" font-family="DejaVu Sans" font-size="13.00">[struct bottom]</text> +</g> +<!-- STRUCT_BOT->BOT --> +<g id="edge62" class="edge"> +<title>STRUCT_BOT->BOT</title> +<path fill="none" stroke="#555555" d="M982.84,-85.85C959.17,-69.85 929.19,-49.57 908.49,-35.56"/> +<polygon fill="#555555" stroke="#555555" points="909.78,-33.9 903.63,-32.28 907.42,-37.38 909.78,-33.9"/> +</g> +<!-- STRUCT_PERSON->STRUCT_BOT --> +<g id="edge40" class="edge"> +<title>STRUCT_PERSON->STRUCT_BOT</title> +<path fill="none" stroke="#555555" d="M957.85,-196.12C968.43,-181.96 982.92,-162.59 995.24,-146.11"/> +<polygon fill="#555555" stroke="#555555" points="996.85,-147.46 998.76,-141.4 993.49,-144.94 996.85,-147.46"/> +</g> +<!-- STRUCT_PERSON->STRUCT_S1 --> +<!-- STRUCT_S1->STRUCT_BOT --> +<g id="edge41" class="edge"> +<title>STRUCT_S1->STRUCT_BOT</title> +<path fill="none" stroke="#555555" d="M1020,-196.12C1020,-182.64 1020,-164.43 1020,-148.49"/> +<polygon fill="#555555" stroke="#555555" points="1022.1,-148.87 1020,-142.87 1017.9,-148.87 1022.1,-148.87"/> +</g> +<!-- STRUCT_S1->STRUCT_S2 --> +<!-- STRUCT_S2->STRUCT_BOT --> +<g id="edge42" class="edge"> +<title>STRUCT_S2->STRUCT_BOT</title> +<path fill="none" stroke="#555555" d="M1091.84,-196.12C1079.5,-181.83 1062.57,-162.25 1048.25,-145.68"/> +<polygon fill="#555555" stroke="#555555" points="1050,-144.5 1044.49,-141.33 1046.82,-147.24 1050,-144.5"/> +</g> +<!-- STRUCT_S2->STRUCT_MORE --> +<!-- STRUCT_MORE->STRUCT_BOT --> +<g id="edge43" class="edge"> +<title>STRUCT_MORE->STRUCT_BOT</title> +<path fill="none" stroke="#555555" d="M1177.76,-198.72C1173.05,-195 1167.95,-191.22 1163,-187.98 1135.19,-169.81 1102.49,-152.39 1075.38,-138.93"/> +<polygon fill="#555555" stroke="#555555" points="1076.64,-137.21 1070.33,-136.44 1074.78,-140.97 1076.64,-137.21"/> +</g> +<!-- MEM_BOT --> +<g id="node27" class="node"> +<title>MEM_BOT</title> +<ellipse fill="#00008b" stroke="black" cx="1294" cy="-111.99" rx="44.7" ry="18"/> +<text text-anchor="middle" x="1294" y="-107.89" font-family="DejaVu Sans" font-size="13.00" fill="white">⊥:mem</text> +</g> +<!-- MEM_TOP->MEM_BOT --> +<g id="edge44" class="edge"> +<title>MEM_TOP->MEM_BOT</title> +<path fill="none" stroke="#555555" d="M1294,-408.67C1294,-356.2 1294,-198.53 1294,-137.49"/> +<polygon fill="#555555" stroke="#555555" points="1296.1,-137.72 1294,-131.72 1291.9,-137.72 1296.1,-137.72"/> +</g> +<!-- MEM_TOP->TUPLE_TOP --> +<!-- MEM_BOT->BOT --> +<g id="edge63" class="edge"> +<title>MEM_BOT->BOT</title> +<path fill="none" stroke="#555555" d="M1273.96,-95.5C1263.49,-88.17 1250.14,-79.95 1237,-75 1125.68,-33.06 983.13,-22.53 918.55,-19.89"/> +<polygon fill="#555555" stroke="#555555" points="918.65,-17.79 912.58,-19.66 918.49,-21.99 918.65,-17.79"/> +</g> +<!-- TUPLE1 --> +<g id="node30" class="node"> +<title>TUPLE1</title> +<polygon fill="#ffc0cb" stroke="black" points="1418.88,-339.96 1363.12,-339.96 1363.12,-303.96 1418.88,-303.96 1418.88,-339.96"/> +<text text-anchor="middle" x="1391" y="-317.86" font-family="DejaVu Sans" font-size="13.00">(type)</text> +</g> +<!-- TUPLE_TOP->TUPLE1 --> +<g id="edge47" class="edge"> +<title>TUPLE_TOP->TUPLE1</title> +<path fill="none" stroke="#555555" d="M1405.19,-408.93C1402.44,-392.26 1398.22,-366.67 1395.09,-347.74"/> +<polygon fill="#555555" stroke="#555555" points="1397.16,-347.4 1394.11,-341.83 1393.02,-348.09 1397.16,-347.4"/> +</g> +<!-- TUPLE2 --> +<g id="node31" class="node"> +<title>TUPLE2</title> +<polygon fill="#ffc0cb" stroke="black" points="1529,-339.96 1435,-339.96 1435,-303.96 1529,-303.96 1529,-339.96"/> +<text text-anchor="middle" x="1482" y="-317.86" font-family="DejaVu Sans" font-size="13.00">(type, type)</text> +</g> +<!-- TUPLE_TOP->TUPLE2 --> +<g id="edge48" class="edge"> +<title>TUPLE_TOP->TUPLE2</title> +<path fill="none" stroke="#555555" d="M1419.89,-409.39C1432.2,-392.26 1451.54,-365.35 1465.37,-346.1"/> +<polygon fill="#555555" stroke="#555555" points="1466.97,-347.48 1468.76,-341.38 1463.56,-345.02 1466.97,-347.48"/> +</g> +<!-- TUPLEN --> +<g id="node32" class="node"> +<title>TUPLEN</title> +<polygon fill="#ffc0cb" stroke="black" points="1622.75,-339.96 1545.25,-339.96 1545.25,-303.96 1622.75,-303.96 1622.75,-339.96"/> +<text text-anchor="middle" x="1584" y="-317.86" font-family="DejaVu Sans" font-size="13.00">(type, …)</text> +</g> +<!-- TUPLE_TOP->TUPLEN --> +<g id="edge49" class="edge"> +<title>TUPLE_TOP->TUPLEN</title> +<path fill="none" stroke="#555555" d="M1432.78,-411.64C1458.78,-396.61 1500.83,-372.23 1537,-350.95 1540.81,-348.7 1544.8,-346.35 1548.76,-344"/> +<polygon fill="#555555" stroke="#555555" points="1549.56,-345.97 1553.64,-341.1 1547.41,-342.36 1549.56,-345.97"/> +</g> +<!-- TUPLE_BOT --> +<g id="node29" class="node"> +<title>TUPLE_BOT</title> +<ellipse fill="#ffc0cb" stroke="black" cx="1445" cy="-111.99" rx="44.7" ry="18"/> +<text text-anchor="middle" x="1445" y="-107.89" font-family="DejaVu Sans" font-size="13.00">⊥:tuple</text> +</g> +<!-- TUPLE_BOT->BOT --> +<g id="edge64" class="edge"> +<title>TUPLE_BOT->BOT</title> +<path fill="none" stroke="#555555" d="M1416.38,-97.91C1398.05,-90.08 1373.56,-80.58 1351,-75 1192.43,-35.77 996.51,-23.66 918.45,-20.23"/> +<polygon fill="#555555" stroke="#555555" points="918.86,-18.15 912.78,-19.99 918.69,-22.35 918.86,-18.15"/> +</g> +<!-- TUPLE1->TUPLE_BOT --> +<g id="edge50" class="edge"> +<title>TUPLE1->TUPLE_BOT</title> +<path fill="none" stroke="#555555" d="M1395.51,-303.58C1405.12,-266.56 1427.65,-179.82 1438.68,-137.32"/> +<polygon fill="#555555" stroke="#555555" points="1440.68,-137.97 1440.16,-131.63 1436.62,-136.91 1440.68,-137.97"/> +</g> +<!-- TUPLE1->TUPLE2 --> +<!-- TUPLE2->TUPLE_BOT --> +<g id="edge51" class="edge"> +<title>TUPLE2->TUPLE_BOT</title> +<path fill="none" stroke="#555555" d="M1478.91,-303.58C1472.32,-266.56 1456.89,-179.82 1449.33,-137.32"/> +<polygon fill="#555555" stroke="#555555" points="1451.44,-137.2 1448.32,-131.66 1447.3,-137.93 1451.44,-137.2"/> +</g> +<!-- TUPLE2->TUPLEN --> +<!-- TUPLEN->TUPLE_BOT --> +<g id="edge52" class="edge"> +<title>TUPLEN->TUPLE_BOT</title> +<path fill="none" stroke="#555555" d="M1572.39,-303.58C1547.38,-266.16 1488.41,-177.94 1460.36,-135.96"/> +<polygon fill="#555555" stroke="#555555" points="1462.21,-134.96 1457.13,-131.14 1458.72,-137.29 1462.21,-134.96"/> +</g> +</g> +</svg> diff --git a/docs/chapter10a/type-control.svg b/docs/chapter10a/type-control.svg new file mode 100644 index 00000000..d4b79c53 --- /dev/null +++ b/docs/chapter10a/type-control.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="61" height="20" viewBox="0 0 61 20" role="img" aria-labelledby="title"> + <title id="title">Control</title> + <rect width="61" height="20" rx="2" fill="#ffff00"/> + <text x="30.5" y="14" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" fill="#000000">Control</text> +</svg> diff --git a/docs/chapter10a/type-integer.svg b/docs/chapter10a/type-integer.svg new file mode 100644 index 00000000..97d03372 --- /dev/null +++ b/docs/chapter10a/type-integer.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="60" height="20" viewBox="0 0 60 20" role="img" aria-labelledby="title"> + <title id="title">Integer</title> + <rect width="60" height="20" rx="2" fill="#add8e6"/> + <text x="30.0" y="14" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" fill="#000000">Integer</text> +</svg> diff --git a/docs/chapter10a/type-memory.svg b/docs/chapter10a/type-memory.svg new file mode 100644 index 00000000..16b5121f --- /dev/null +++ b/docs/chapter10a/type-memory.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="67" height="20" viewBox="0 0 67 20" role="img" aria-labelledby="title"> + <title id="title">Memory</title> + <rect width="67" height="20" rx="2" fill="#00008b"/> + <text x="33.5" y="14" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" fill="#ffffff">Memory</text> +</svg> diff --git a/docs/chapter10a/type-pointer.svg b/docs/chapter10a/type-pointer.svg new file mode 100644 index 00000000..7604492e --- /dev/null +++ b/docs/chapter10a/type-pointer.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="62" height="20" viewBox="0 0 62 20" role="img" aria-labelledby="title"> + <title id="title">Pointer</title> + <rect width="62" height="20" rx="2" fill="#008000"/> + <text x="31.0" y="14" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" fill="#ffffff">Pointer</text> +</svg> diff --git a/docs/chapter10a/type-struct.svg b/docs/chapter10a/type-struct.svg new file mode 100644 index 00000000..97a16562 --- /dev/null +++ b/docs/chapter10a/type-struct.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="52" height="20" viewBox="0 0 52 20" role="img" aria-labelledby="title"> + <title id="title">Struct</title> + <rect width="52" height="20" rx="2" fill="#90ee90"/> + <text x="26.0" y="14" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" fill="#000000">Struct</text> +</svg> diff --git a/docs/chapter10a/type-tuple.svg b/docs/chapter10a/type-tuple.svg new file mode 100644 index 00000000..691478ac --- /dev/null +++ b/docs/chapter10a/type-tuple.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="47" height="20" viewBox="0 0 47 20" role="img" aria-labelledby="title"> + <title id="title">Tuple</title> + <rect width="47" height="20" rx="2" fill="#ffc0cb"/> + <text x="23.5" y="14" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" fill="#000000">Tuple</text> +</svg> diff --git a/src/main/java/com/seaofnodes/simple/GraphEvaluator.java b/src/main/java/com/seaofnodes/simple/GraphEvaluator.java deleted file mode 100644 index 9fc51c8e..00000000 --- a/src/main/java/com/seaofnodes/simple/GraphEvaluator.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.TypeInteger; - -import java.util.Arrays; -import java.util.BitSet; -import java.util.HashMap; - -public class GraphEvaluator { - - public enum ResultType { VALUE, FALLTHROUGH, TIMEOUT } - - public record Result(ResultType type, long value) {} - - /** - * Find the start node from some node in the graph or null if there is no start node - */ - private static StartNode findStart(BitSet visit, Node node) { - if (node == null) return null; - if (node instanceof StartNode start) return start; - if( visit.get(node._nid) ) return null; - visit.set(node._nid); - for( Node def : node._inputs ) { - var res = findStart(visit,def); - if( res != null ) return res; - } - for( Node use : node._outputs ) { - var res = findStart(visit,use); - if( res != null ) return res; - } - return null; - } - - /** - * Find the control output from a control node - */ - private static Node findControl(Node control) { - for( Node use : control._outputs ) { - if (use.isCFG()) return use; - } - return null; - } - - /** - * Find the projection for a node - */ - private static ProjNode findProjection(Node node, int idx) { - for( Node use : node._outputs ) { - if (use instanceof ProjNode proj && proj._idx == idx) return proj; - } - return null; - } - - /** - * Cache values for phi and parameter projection nodes. - */ - private final HashMap<Node, Long> cacheValues = new HashMap<>(); - /** - * Cache for loop phis as they can depend on itself or other loop phis - */ - private long[] loopPhiCache = new long[16]; - - private GraphEvaluator() { - } - - private long div(DivNode div) { - long in2 = getValue(div.in(2)); - return in2 == 0 ? 0 : getValue(div.in(1)) / in2; - } - - /** - * Calculate the value of a node - */ - private long getValue(Node node) { - var cache = cacheValues.get(node); - if (cache != null) return cache; - return switch (node) { - case ConstantNode cons -> ((TypeInteger)cons.compute()).value(); - case AddNode add -> getValue(add.in(1)) + getValue(add.in(2)); - case BoolNode.EQ eq -> getValue(eq.in(1)) == getValue(eq.in(2)) ? 1 : 0; - case BoolNode.LE le -> getValue(le.in(1)) <= getValue(le.in(2)) ? 1 : 0; - case BoolNode.LT lt -> getValue(lt.in(1)) < getValue(lt.in(2)) ? 1 : 0; - case DivNode div -> div(div); - case MinusNode minus -> -getValue(minus.in(1)); - case MulNode mul -> getValue(mul.in(1)) * getValue(mul.in(2)); - case NotNode not -> getValue(not.in(1)) == 0 ? 1 : 0; - case SubNode sub -> getValue(sub.in(1)) - getValue(sub.in(2)); - default -> throw Utils.TODO("Unexpected node " + node); - }; - } - - /** - * Special case of latchPhis when phis can depend on phis of the same region. - */ - private void latchLoopPhis(RegionNode region, Node prev) { - int idx = Utils.find(region._inputs, prev); - assert idx > 0; - int i = 0; - for( Node use : region._outputs ) { - if (use instanceof PhiNode phi) { - var value = getValue(phi.in(idx)); - if (i == loopPhiCache.length) loopPhiCache = Arrays.copyOf(loopPhiCache, loopPhiCache.length*2); - loopPhiCache[i++] = value; - } - } - i = 0; - for( Node use : region._outputs ) { - if (use instanceof PhiNode phi) cacheValues.put(phi, loopPhiCache[i++]); - } - } - - /** - * Calculate the values of phis of the region and caches the values. The phis are not allowed to depend on other phis of the region. - */ - private void latchPhis(RegionNode region, Node prev) { - int idx = Utils.find(region._inputs, prev); - assert idx > 0; - for( Node use : region._outputs ) { - if (use instanceof PhiNode phi) { - var value = getValue(phi.in(idx)); - cacheValues.put(phi, value); - } - } - } - - /** - * Run the graph until either a return is found or the number of loop iterations are done. - */ - private Result evaluate(StartNode start, long parameter, int loops) { - var parameter1 = findProjection(start, 1); - if (parameter1 != null) cacheValues.put(parameter1, parameter); - Node control = findProjection(start, 0); - Node prev = start; - while (control != null) { - Node next; - switch (control) { - case RegionNode region -> { - if (region instanceof LoopNode && region.in(1) != prev) { - if (loops--<=0) return new Result(ResultType.TIMEOUT, 0); - latchLoopPhis(region, prev); - } else { - latchPhis(region, prev); - } - next = findControl(region); - } - case IfNode cond -> next = findProjection(cond, getValue(cond.in(1)) != 0 ? 0 : 1); - case ReturnNode ret -> { - return new Result(ResultType.VALUE, getValue(ret.in(1))); - } - case ProjNode ignored -> next = findControl(control); - default -> throw Utils.TODO("Unexpected control node " + control); - } - prev = control; - control = next; - } - return new Result(ResultType.FALLTHROUGH, 0); - } - - public static long evaluate(Node graph) { - return evaluate(graph, 0); - } - - public static long evaluate(Node graph, long parameter) { - return evaluate(graph, parameter, 1000); - } - - public static long evaluate(Node graph, long parameter, int loops) { - var res = evaluateWithResult(graph, parameter, loops); - if (res.type== ResultType.TIMEOUT) throw new RuntimeException("Timeout"); - return res.value; - } - - public static Result evaluateWithResult(Node graph, long parameter, int loops) { - var start = findStart(new BitSet(), graph); - if (start == null) return new Result(ResultType.TIMEOUT, 0); - var evaluator = new GraphEvaluator(); - return evaluator.evaluate(start, parameter, loops); - } - -} diff --git a/src/main/java/com/seaofnodes/simple/IRPrinter.java b/src/main/java/com/seaofnodes/simple/IRPrinter.java index c06ac9d0..1ac32b1a 100644 --- a/src/main/java/com/seaofnodes/simple/IRPrinter.java +++ b/src/main/java/com/seaofnodes/simple/IRPrinter.java @@ -2,14 +2,77 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.node.LoopNode; import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.ProjNode; import java.util.ArrayList; import java.util.BitSet; public class IRPrinter { - // Another bulk pretty-printer. Makes more effort at basic-block grouping. + // Print a node on 1 line, columnar aligned, as: + // NNID NNAME DDEF DDEF [[ UUSE UUSE ]] TYPE + // 1234 sssss 1234 1234 1234 1234 1234 1234 tttttt + public static void _printLine( Node n, StringBuilder sb ) { + sb.append("%4d %-7.7s ".formatted(n._nid,n.label())); + if( n.isDead() ) { + sb.append("DEAD\n"); + return; + } + for( Node def : n._inputs ) + sb.append(def==null ? "____ " : "%4d ".formatted(def._nid)); + for( int i = n._inputs.size(); i<3; i++ ) + sb.append(" "); + sb.append(" [[ "); + for( Node use : n._outputs ) + sb.append(use==null ? "____ " : "%4d ".formatted(use._nid)); + int lim = 5 - Math.max(n._inputs.size(),3); + for( int i = n._outputs.size(); i<lim; i++ ) + sb.append(" "); + sb.append(" ]] "); + if( n._type!= null ) sb.append(n._type.str()); + sb.append("\n"); + } + + private static StringBuilder nodeId(StringBuilder sb, Node n) { + sb.append("%%%d".formatted(n._nid)); + if (n instanceof ProjNode proj) { + sb.append(".").append(proj._idx); + } + return sb; + } + + // Print a node on 1 line, format is inspired by LLVM + // %id: TYPE = NODE(inputs ....) + // Nodes as referred to as %id + public static void _printLineLlvmFormat( Node n, StringBuilder sb ) { + nodeId(sb, n).append(": "); + if( n.isDead() ) { + sb.append("DEAD\n"); + return; + } + if( n._type!= null ) n._type.typeName(sb); + sb.append(" = ").append( n.label() ).append( "(" ); + for( int i = 0; i < n._inputs.size(); i++ ) { + Node def = n.in(i); + if (i > 0) + sb.append(", "); + if (def == null) sb.append("_"); + else nodeId(sb, def); + } + sb.append(")").append("\n"); + } + + public static void printLine( Node n, StringBuilder sb, boolean llvmFormat ) { + if (llvmFormat) _printLineLlvmFormat( n, sb ); + else _printLine ( n, sb ); + } + public static String prettyPrint(Node node, int depth) { + return prettyPrint( node, depth, false ); + } + + // Another bulk pretty-printer. Makes more effort at basic-block grouping. + public static String prettyPrint( Node node, int depth, boolean llvmFormat ) { // First, a Breadth First Search at a fixed depth. BFS bfs = new BFS(node,depth); // Convert just that set to a post-order @@ -24,16 +87,16 @@ public class IRPrinter { Node n = rpos.get(i); if( n.isCFG() || n.isMultiHead() ) { if( !gap ) sb.append("\n"); // Blank before multihead - n._printLine(sb); // Print head + printLine( n, sb, llvmFormat ); // Print head while( --i >= 0 ) { Node t = rpos.get(i); if( !t.isMultiTail() ) { i++; break; } - t._printLine(sb); + printLine( t, sb, llvmFormat ); } sb.append("\n"); // Blank after multitail gap = true; } else { - n._printLine( sb ); + printLine( n, sb, llvmFormat ); gap = false; } } diff --git a/src/main/java/com/seaofnodes/simple/IterPeeps.java b/src/main/java/com/seaofnodes/simple/IterPeeps.java index 41fc6916..7075d526 100644 --- a/src/main/java/com/seaofnodes/simple/IterPeeps.java +++ b/src/main/java/com/seaofnodes/simple/IterPeeps.java @@ -1,8 +1,6 @@ package com.seaofnodes.simple; -import com.seaofnodes.simple.node.ConstantNode; -import com.seaofnodes.simple.node.Node; -import com.seaofnodes.simple.node.StopNode; +import com.seaofnodes.simple.node.*; import java.util.ArrayList; import java.util.Arrays; @@ -95,6 +93,8 @@ public abstract class IterPeeps { n.moveDepsToWorklist(); assert progressOnList(stop); // Very expensive assert } + if( n.isUnused() && !(n instanceof StopNode) ) + n.kill(); // Just plain dead if( obs != null ) obs.after(n, x, true); } diff --git a/src/main/java/com/seaofnodes/simple/Parser.java b/src/main/java/com/seaofnodes/simple/Parser.java index 3c1eab96..7734f4df 100644 --- a/src/main/java/com/seaofnodes/simple/Parser.java +++ b/src/main/java/com/seaofnodes/simple/Parser.java @@ -53,14 +53,17 @@ public class Parser { /** * List of keywords disallowed as identifiers */ - private final HashSet<String> KEYWORDS = new HashSet<>(){{ + private static final HashSet<String> KEYWORDS = new HashSet<>(){{ add("break"); add("continue"); add("else"); add("false"); add("if"); add("int"); + add("new"); + add("null"); add("return"); + add("struct"); add("true"); add("while"); }}; @@ -75,14 +78,18 @@ public class Parser { ScopeNode _continueScope; ScopeNode _breakScope; + // Mapping from a Struct name to a Struct. + public static Map<String, TypeStruct> OBJS = new HashMap<>(); + public Parser(String source, TypeInteger arg) { PARSER = this; Node.reset(); IterPeeps.reset(); + OBJS.clear(); _lexer = new Lexer(source); _scope = new ScopeNode(); _continueScope = _breakScope = null; - START = new StartNode(new Type[]{ Type.CONTROL, arg }); + START = new StartNode(new Type[]{ Type.CONTROL, TypeMem.BOT, arg }); STOP = new StopNode(source); } @@ -106,8 +113,9 @@ public class Parser { _xScopes.push(_scope); // Enter a new scope for the initial control and arguments _scope.push(); - _scope.define(ScopeNode.CTRL, new ProjNode(START, 0, ScopeNode.CTRL).peephole()); - _scope.define(ScopeNode.ARG0, new ProjNode(START, 1, ScopeNode.ARG0).peephole()); + _scope.define(ScopeNode.CTRL, Type.CONTROL , new ProjNode(START, 0, ScopeNode.CTRL).peephole()); + _scope.define(ScopeNode.ARG0, TypeInteger.BOT, new ProjNode(START, 2, ScopeNode.ARG0).peephole()); + _scope.define("$mem", TypeMem.BOT, new ProjNode(START, 1, "$mem").peephole()); parseBlock(); _scope.pop(); _xScopes.pop(); @@ -144,18 +152,59 @@ public class Parser { * @return a {@link Node} or {@code null} */ private Node parseStatement() { - if (matchx("return") ) return parseReturn(); - else if (matchx("int")) return parseDecl(); - else if (match ("{" )) return require(parseBlock(),"}"); - else if (matchx("if" )) return parseIf(); - else if (matchx("while")) return parseWhile(); - else if (matchx("break")) return parseBreak(); + if( false ) return null; + else if (matchx("return") ) return parseReturn(); + else if (matchx("int") ) return parseDecl(TypeInteger.BOT); + else if (match ("{") ) return require(parseBlock(),"}"); + else if (matchx("if") ) return parseIf(); + else if (matchx("while") ) return parseWhile(); + else if (matchx("break") ) return parseBreak(); else if (matchx("continue")) return parseContinue(); - else if (matchx(";")) return null; // Empty statement + else if (matchx("struct") ) return parseStruct(); + else if (matchx(";") ) return null; // Empty statement + // declarations of vars with struct type are handled in parseExpressionStatement due + // to ambiguity else return parseExpressionStatement(); } /** + * Parse a struct field. + * <pre> + * int IDENTIFIER ; + * </pre> + */ + private Field parseField(String sname) { + if( matchx("int") ) // Currently only parsing "int" type fields + return require(Field.make(requireId().intern(),TypeInteger.BOT),";"); + throw errorSyntax("A field type is expected, only type 'int' is supported at present"); + } + + /** + * Parse a struct declaration, and return the following statement. + * Only allowed in top level scope. + * Structs cannot be redefined. + * + * @return The statement following the struct + */ + private Node parseStruct() { + if (_xScopes.size() > 1) throw errorSyntax("struct declarations can only appear in top level scope"); + String typeName = requireId(); + if ( OBJS.containsKey(typeName)) throw errorSyntax("struct '" + typeName + "' cannot be redefined"); + ArrayList<Field> fields = new ArrayList<>(); + require("{"); + while (!peek('}') && !_lexer.isEOF()) { + Field field = parseField(typeName); + if (fields.contains(field)) throw errorSyntax("Field '" + field + "' already defined in struct '" + typeName + "'"); + fields.add(field); + } + require("}"); + // Build and install the TypeStruct + TypeStruct ts = TypeStruct.make(typeName, fields); + OBJS.put(typeName, ts); // Insert the struct name in the collection of all struct names + return parseStatement(); + } + + /** * Parses a while statement * * <pre> @@ -247,7 +296,7 @@ public class Parser { cur.pop(); // If this is a continue then first time the target is null // So we just use the pruned current scope as the base for the - // continue + // "continue" if (toScope == null) return cur; // toScope is either the break scope, or a scope that was created here @@ -272,7 +321,7 @@ public class Parser { private Node parseIf() { require("("); // Parse predicate - var pred = require(parseExpression(), ")"); + var pred = require(parseExpression(), ")").keep(); // IfNode takes current control and predicate Node ifNode = new IfNode(ctrl(), pred).peephole(); // Setup projection nodes @@ -287,6 +336,7 @@ public class Parser { // Parse the true side ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection + _scope.upcast(ifT,pred,false); // Up-cast predicate parseStatement(); // Parse true-side ScopeNode tScope = _scope; @@ -294,9 +344,11 @@ public class Parser { _scope = fScope; // Restore scope, then parse else block if any ifF.unkeep(); // fScope already owns the false control if (matchx("else")) { + _scope.upcast(ifF,pred,true); // Up-cast predicate parseStatement(); fScope = _scope; } + pred.unkeep(); if( tScope.nIns() != ndefs || fScope.nIns() != ndefs ) throw error("Cannot define a new name on one arm of an if"); @@ -320,7 +372,7 @@ public class Parser { */ private Node parseReturn() { var expr = require(parseExpression(), ";"); - Node ret = STOP.addReturn(new ReturnNode(ctrl(), expr).peephole()); + Node ret = STOP.addReturn(new ReturnNode(ctrl(), expr, _scope).peephole()); ctrl(new ConstantNode(Type.XCONTROL).peephole()); // Kill control return ret; } @@ -328,40 +380,86 @@ public class Parser { /** - * Parses an expression statement + * Parses an expression statement or a declaration statement where type is a struct * * <pre> - * name '=' expression ';' + * name; // Error + * type name; // Define name with default initial value + * type name = expr; // Define name with given initial value + * name = expr; // Reassign existing + * expr // Something else * </pre> * @return an expression {@link Node}, never {@code null} */ private Node parseExpressionStatement() { - var name = requireId(); - require("="); - var expr = require(parseExpression(), ";"); - if( _scope.update(name, expr)==null ) - throw error("Undefined name '" + name + "'"); + int old = _lexer._position; + Type t = type(); + String name = requireId(); + Node expr; + if( match(";") ) { // Assign a default value + // No type and no expr is an error + if( t==null ) throw errorSyntax("expression"); + expr = new ConstantNode(t.makeInit()).peephole(); + } else if( match("=") ) { // Assign "= expr;" + expr = require(parseExpression(), ";"); + } else { // Neither, so just a normal expression parse + _lexer._position = old; + return require(parseExpression(),";"); + } + + // Defining a new variable vs updating an old one + if( t != null ) { + if( _scope.define(name,t,expr) == null ) + throw error("Redefining name '" + name + "'"); + } else { + Node n = _scope.lookup(name); + t = _scope.lookupDeclaredType(name); + if( n==null ) + throw error("Undefined name '" + name + "'"); + _scope.update(name,expr); + } + if( !expr._type.isa(t) ) + throw error("Type " + expr._type.str() + " is not of declared type " + t.str()); return expr; } + // Parse a type-or-null + private Type type() { + int old = _lexer._position; + String tname = _lexer.matchId(); + if( tname==null ) return null; + if( tname.equals("int") ) return TypeInteger.BOT; + TypeStruct obj = OBJS.get(tname); + if( obj != null ) + return TypeMemPtr.make(obj,match("?")); + // Not a type; unwind the parse + _lexer._position = old; + return null; + } + /** * Parses a declStatement * * <pre> - * 'int' name = expression ';' + * type name = expression ';' * </pre> - * @return an expression {@link Node}, never {@code null} + * @return an expression {@link Node} */ - private Node parseDecl() { - // Type is 'int' for now + private Node parseDecl(Type t) { var name = requireId(); - require("="); - var expr = require(parseExpression(), ";"); - if( _scope.define(name,expr) == null ) + var expr = match(";") + // Assign a null value + ? new ConstantNode(t.makeInit()).peephole() + // Assign "= expr;" + : require(require("=").parseExpression(), ";"); + if( !expr._type.isa(t) ) + throw error("Type " + expr._type.str() + " is not of declared type " + t.str()); + if( _scope.define(name,t,expr) == null ) throw error("Redefining name '" + name + "'"); return expr; } + /** * Parse an expression of the form: * @@ -448,20 +546,21 @@ public class Parser { * Parse a unary minus expression. * * <pre> - * unaryExpr : ('-') unaryExpr | primaryExpr + * unaryExpr : ('-') | '!') unaryExpr | postfixExpr | primaryExpr * </pre> * @return a unary expression {@link Node}, never {@code null} */ private Node parseUnary() { if (match("-")) return new MinusNode(parseUnary()).peephole(); - return parsePrimary(); + if (match("!")) return new NotNode(parseUnary()).peephole(); + return parsePostfix(parsePrimary()); } /** * Parse a primary expression: * * <pre> - * primaryExpr : integerLiteral | Identifier | true | false | '(' expression ')' + * primaryExpr : integerLiteral | Identifier | true | false | null | new Identifier | '(' expression ')' * </pre> * @return a primary {@link Node}, never {@code null} */ @@ -470,6 +569,13 @@ public class Parser { if( match("(") ) return require(parseExpression(), ")"); if( matchx("true" ) ) return new ConstantNode(TypeInteger.constant(1)).peephole(); if( matchx("false") ) return new ConstantNode(TypeInteger.constant(0)).peephole(); + if( matchx("null" ) ) return new ConstantNode(TypeMemPtr.NULLPTR).peephole(); + if( matchx("new") ) { + String structName = requireId(); + TypeStruct obj = OBJS.get(structName); + if( obj == null) throw errorSyntax("Unknown struct type '" + structName + "'"); + return newStruct(obj); + } String name = _lexer.matchId(); if( name == null) throw errorSyntax("an identifier or expression"); Node n = _scope.lookup(name); @@ -478,6 +584,54 @@ public class Parser { } /** + * Return a NewNode but also generate instructions to initialize it. + */ + private Node newStruct( TypeStruct obj ) { + Node n = new NewNode(TypeMemPtr.make(obj), ctrl()).peephole().keep(); + Node initValue = new ConstantNode(TypeInteger.constant(0)).peephole(); + for( Field field : obj._fields ) { + mem(new StoreNode(field._fname, mem(), n, initValue).peephole()); + } + return n.unkeep(); + } + + // Memory is one hidden SSA variable, including across branches and loops. + private Node mem() { return _scope.lookup("$mem"); } + private Node mem(Node n) { return _scope.update("$mem",n); } + + /** + * Parse postfix expression. For now this is just a field + * expression, but in future could be array index too. + * + * <pre> + * expr ('.' IDENTIFIER)* [ = expr ] + * </pre> + */ + private Node parsePostfix(Node expr) { + if( !match(".") ) return expr; + + if( !(expr._type instanceof TypeMemPtr ptr) ) + throw error("Expected struct reference but got " + expr._type.str()); + + String name = requireId().intern(); + int idx = ptr._obj==null ? -1 : ptr._obj.find(name); + if( idx == -1 ) throw error("Accessing unknown field '" + name + "' from '" + ptr.str() + "'"); + + if( match("=") ) { + // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment + if( peek('=') ) _lexer._position--; + else { + Node val = parseExpression(); + mem(new StoreNode(name, mem(), expr, val).peephole()); + return expr; // "obj.a = expr" returns the expression while updating memory + } + } + + Type declaredType = ptr._obj._fields[idx]._type; + return parsePostfix(new LoadNode(name, declaredType, mem(), expr).peephole()); + } + + /** * Parse integer literal * * <pre> @@ -497,6 +651,7 @@ public class Parser { private boolean matchx(String syntax) { return _lexer.matchx(syntax); } // Return true and do NOT skip if 'ch' is next private boolean peek(char ch) { return _lexer.peek(ch); } + private boolean peekIsId() { return _lexer.peekIsId(); } // Require and return an identifier private String requireId() { @@ -505,9 +660,11 @@ public class Parser { throw error("Expected an identifier, found '"+id+"'"); } + + // Require an exact match - private void require(String syntax) { require(null, syntax); } - private <N extends Node> N require(N n, String syntax) { + private Parser require(String syntax) { require(null, syntax); return this; } + private <N> N require(N n, String syntax) { if (match(syntax)) return n; throw errorSyntax(syntax); } @@ -594,6 +751,8 @@ public class Parser { return true; } + // Match must be exact and not followed by more ID characters. + // Prevents identifier "ifxy" from matching an "if" statement. boolean matchx(String syntax) { if( !match(syntax) ) return false; if( !isIdLetter(peek()) ) return true; @@ -606,10 +765,14 @@ public class Parser { return peek()==ch; } + boolean peekIsId() { + skipWhiteSpace(); + return isIdStart(peek()); + } + // Return an identifier or null String matchId() { - skipWhiteSpace(); - return isIdStart(peek()) ? parseId() : null; + return peekIsId() ? parseId() : null; } // Used for errors diff --git a/src/main/java/com/seaofnodes/simple/Utils.java b/src/main/java/com/seaofnodes/simple/Utils.java index 45f15327..4f2af769 100644 --- a/src/main/java/com/seaofnodes/simple/Utils.java +++ b/src/main/java/com/seaofnodes/simple/Utils.java @@ -36,4 +36,30 @@ public class Utils { return i; return -1; } + /** + * Search a list for an element by reference + * + * @param ary List to search in + * @param x Object to be searched + * @return >= 0 on success, -1 on failure + */ + public static <E> int find( E[] ary, E x ) { + for( int i=0; i<ary.length; i++ ) + if( ary[i]==x ) + return i; + return -1; + } + + // Rotate a long, nice for hashes + public static long rot( long x, int n ) { return (x<<n) | (x>>>n); } + + // Fold a 64bit hash into 32 bits + public static int fold( long x ) { return (int)((x>>32) ^ x); } + + public static boolean eq(String s0, String s1) { + if( s0==s1 ) return true; + assert s0==s0.intern(); + assert s1==s1.intern(); + return false; + } } diff --git a/src/main/java/com/seaofnodes/simple/node/AddNode.java b/src/main/java/com/seaofnodes/simple/node/AddNode.java index b57b305f..de5044d7 100644 --- a/src/main/java/com/seaofnodes/simple/node/AddNode.java +++ b/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -134,7 +134,7 @@ public class AddNode extends Node { for( int i=1; i<ns.length; i++ ) ns[i] = op.copy(lphi.in(i), rhs instanceof PhiNode ? rhs.in(i) : rhs).peephole(); String label = lphi._label + (rhs instanceof PhiNode rphi ? rphi._label : ""); - Node phi = new PhiNode(label,ns).peephole(); + Node phi = new PhiNode(label,lphi._declaredType,ns).peephole(); // Rotate needs another op, otherwise just the phi return lhs==lphi ? phi : op.copy(lhs.in(1),phi); } diff --git a/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/src/main/java/com/seaofnodes/simple/node/BoolNode.java index 08ff3789..bf0d3a84 100644 --- a/src/main/java/com/seaofnodes/simple/node/BoolNode.java +++ b/src/main/java/com/seaofnodes/simple/node/BoolNode.java @@ -2,6 +2,7 @@ package com.seaofnodes.simple.node; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; import java.util.BitSet; @@ -45,11 +46,25 @@ abstract public class BoolNode extends Node { if( in(1)==in(2) ) return new ConstantNode(TypeInteger.constant(doOp(3,3)?1:0)); + // Equals pushes constant to the right; 5==X becomes X==5. + if( this instanceof EQ ) { + if( !(in(2) instanceof ConstantNode) ) { + // con==noncon becomes noncon==con + if( in(1) instanceof ConstantNode ) + return new EQ(in(2),in(1)); + // Equals sorts by NID otherwise: non.high == non.low becomes non.low == non.high + else if( in(1)._nid > in(2)._nid ) + return new EQ(in(2),in(1)); + } + // Equals X==0 becomes a !X + if( (in(2)._type == TypeInteger.ZERO || in(2)._type == TypeMemPtr.NULLPTR) ) + return new NotNode(in(1)); + } + // Do we have ((phi cons) cmp con) ? // Do we have ((phi cons) cmp (phi cons)) ? // Push the compare up through the phi: (phi con0 cmp con, con1 cmp con...) - // No rotate: compares are not associative. - Node phicon = AddNode.phiCon(this,false); + Node phicon = AddNode.phiCon(this,this instanceof EQ); if( phicon!=null ) return phicon; return null; diff --git a/src/main/java/com/seaofnodes/simple/node/CastNode.java b/src/main/java/com/seaofnodes/simple/node/CastNode.java new file mode 100644 index 00000000..fec540f7 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/CastNode.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.IterPeeps; + +import java.util.BitSet; + +// Upcast (join) the input to a t. Used after guard test to lift an input. +// Can also be used to make a type-assertion if ctrl is null. +public class CastNode extends Node { + private final Type _t; + public CastNode(Type t, Node ctrl, Node in) { + super(ctrl, in); + _t = t; + setType(compute()); + } + + @Override public String label() { return "("+_t.str()+")"; } + + @Override + public String uniqueName() { return "Cast_" + _nid; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return in(1)._print0(sb.append(label()), visited); + } + + @Override + public Type compute() { + return in(1)._type.join(_t); + } + + @Override + public Node idealize() { + return in(1)._type.isa(_t) ? in(1) : null; + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java index 8fd955e9..d760c0df 100644 --- a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java @@ -51,9 +51,4 @@ public class ConstantNode extends Node { @Override int hash() { return _con.hashCode(); } - - // Dead control is its own idom root - Node idom() { return null; } - @Override int idepth() { return 0; } - } diff --git a/src/main/java/com/seaofnodes/simple/node/IfNode.java b/src/main/java/com/seaofnodes/simple/node/IfNode.java index 460dce51..1cf4ddaf 100644 --- a/src/main/java/com/seaofnodes/simple/node/IfNode.java +++ b/src/main/java/com/seaofnodes/simple/node/IfNode.java @@ -1,15 +1,16 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.type.TypeTuple; - import java.util.BitSet; public class IfNode extends MultiNode { public IfNode(Node ctrl, Node pred) { super(ctrl, pred); + IterPeeps.add(this); // Because idoms are complex, just add it } @Override diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java new file mode 100644 index 00000000..54689283 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -0,0 +1,72 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; + +import java.util.BitSet; + +/** + * Load represents extracting a value from inside a memory object, + * in chapter 10 this means Struct fields. + */ +public class LoadNode extends MemOpNode { + + Type _declaredType; + /** + * Load a value from a ptr.field. + * + * @param name The field we are loading + * @param memSlice The whole-memory node - this is updated after a Store + * @param memPtr The ptr to the struct from where we load a field + */ + public LoadNode(String name, Type glb, Node memSlice, Node memPtr) { + super(name, memSlice, memPtr, null); + _declaredType = glb; + } + + @Override + public String label() { return "Load"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { return sb.append(".").append(_name); } + + @Override + public Type compute() { + return _declaredType; + } + + @Override + public Node idealize() { + + // Simple Load-after-Store on same address. + if( mem() instanceof StoreNode st && + ptr() == st.ptr() && _name.equals(st._name) ) { // Must check same object + return st.val(); + } + + // Push a Load up through a Phi, as long as it collapses on at least + // one arm. If at a Loop, the backedge MUST collapse - else we risk + // spinning the same transform around the loop indefinitely. + // BEFORE (2 Sts, 1 Ld): AFTER (1 St, 0 Ld): + // if( pred ) ptr.x = e0; val = pred ? e0 + // else ptr.x = e1; : e1; + // val = ptr.x; ptr.x = val; + if( mem() instanceof PhiNode memphi && memphi.region()._type == Type.CONTROL && memphi.nIns()== 3 ) { + // Profit on RHS/Loop backedge + if( profit(memphi,2) || + // Else must not be a loop to count profit on LHS. + (!(memphi.region() instanceof LoopNode) && profit(memphi,1)) ) { + Node ld1 = new LoadNode(_name,_declaredType,memphi.in(1),ptr()).peephole(); + Node ld2 = new LoadNode(_name,_declaredType,memphi.in(2),ptr()).peephole(); + return new PhiNode(_name,_type,memphi.region(),ld1,ld2); + } + } + + return null; + } + + // Profitable if we find a matching Store on this Phi arm. + private boolean profit(PhiNode phi, int idx) { + Node px = phi.in(idx); + return px!=null && px.addDep(this) instanceof StoreNode st1 && ptr()==st1.ptr() && _name.equals(st1._name); + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/src/main/java/com/seaofnodes/simple/node/LoopNode.java index fa9d871e..a0ba973e 100644 --- a/src/main/java/com/seaofnodes/simple/node/LoopNode.java +++ b/src/main/java/com/seaofnodes/simple/node/LoopNode.java @@ -25,7 +25,7 @@ public class LoopNode extends RegionNode { return inProgress() ? null : super.idealize(); } + // Bypass Region idom, same as the default idom() using use entry in(1) instead of in(0) @Override int idepth() { return _idepth!=0 ? _idepth : cacheIDepth(entry().idepth()+1); } - @Override Node idom() { return entry(); } } diff --git a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java new file mode 100644 index 00000000..d811e71a --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java @@ -0,0 +1,37 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMemPtr; + +/** + * Convenience common base for Load and Store. + */ +public abstract class MemOpNode extends Node { + + public final String _name; + + public MemOpNode(String name, Node memSlice, Node memPtr, Node value) { + super(null, memSlice, memPtr, value); + _name = name; + } + + public Node mem() { return in(1); } + public Node ptr() { return in(2); } + + @Override + boolean eq(Node n) { + MemOpNode mem = (MemOpNode)n; // Invariant + return _name.equals(mem._name); + } + + @Override + int hash() { return _name.hashCode(); } + + @Override + String err() { + Type ptr = ptr()._type; + return (ptr==Type.BOTTOM || (ptr instanceof TypeMemPtr tmp && tmp._nil) ) + ? "Might be null accessing '" + _name + "'" + : null; + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/MinusNode.java b/src/main/java/com/seaofnodes/simple/node/MinusNode.java index 5b055089..8b1b73bf 100644 --- a/src/main/java/com/seaofnodes/simple/node/MinusNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MinusNode.java @@ -24,5 +24,11 @@ public class MinusNode extends Node { } @Override - public Node idealize() { return null; } + public Node idealize() { + // -(-x) is x + if( in(1) instanceof MinusNode minus ) + return minus.in(1); + + return null; + } } diff --git a/src/main/java/com/seaofnodes/simple/node/NewNode.java b/src/main/java/com/seaofnodes/simple/node/NewNode.java new file mode 100644 index 00000000..b13462f5 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/NewNode.java @@ -0,0 +1,37 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMemPtr; + +import java.util.BitSet; + +public class NewNode extends Node { + + TypeMemPtr _ptr; + + public NewNode(TypeMemPtr ptr, Node ctrl) { + super(ctrl); + this._ptr = ptr; + } + + @Override + public String label() { return "new"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("new "); + return sb.append(_ptr._obj.str()); + } + + @Override + public Type compute() { return _ptr; } + + @Override + public Node idealize() { return null; } + + @Override + boolean eq(Node n) { return this == n; } + + @Override + int hash() { return _ptr.hashCode(); } +} diff --git a/src/main/java/com/seaofnodes/simple/node/Node.java b/src/main/java/com/seaofnodes/simple/node/Node.java index a0ec0289..eb6c1697 100644 --- a/src/main/java/com/seaofnodes/simple/node/Node.java +++ b/src/main/java/com/seaofnodes/simple/node/Node.java @@ -53,15 +53,6 @@ public abstract class Node { /** - * Immediate dominator tree depth, used to approximate a real IDOM during - * parsing where we do not have the whole program, and also peepholes - * change the CFG incrementally. - * <p> - * See {@link <a href="https://en.wikipedia.org/wiki/Dominator_(graph_theory)">...</a>} - */ - char _idepth; - - /** * A private Global Static mutable counter, for unique node id generation. * To make the compiler multithreaded, this field will have to move into a TLS. * Starting with value 1, to avoid bugs confusing node ID 0 with uninitialized values. @@ -83,7 +74,9 @@ public abstract class Node { public abstract String label(); // Unique label for debugging, e.g. "Add12" or "Region30" or "EQ99" - public String uniqueName() { return label() + _nid; } + public String uniqueName() { + return label() + _nid; + } // ------------------------------------------------------------------------ @@ -116,31 +109,6 @@ public abstract class Node { // Every Node implements this; a partial-line recursive print abstract StringBuilder _print1(StringBuilder sb, BitSet visited); - - // Print a node on 1 line, columnar aligned, as: - // NNID NNAME DDEF DDEF [[ UUSE UUSE ]] TYPE - // 1234 sssss 1234 1234 1234 1234 1234 1234 tttttt - public void _printLine(StringBuilder sb ) { - sb.append("%4d %-7.7s ".formatted(_nid,label())); - if( isDead() ) { - sb.append("DEAD\n"); - return; - } - for( Node def : _inputs ) - sb.append(def==null ? "____ " : "%4d ".formatted(def._nid)); - for( int i = _inputs.size(); i<3; i++ ) - sb.append(" "); - sb.append(" [[ "); - for( Node use : _outputs ) - sb.append(use==null ? "____ " : "%4d ".formatted(use._nid)); - int lim = 5 - Math.max(_inputs.size(),3); - for( int i = _outputs.size(); i<lim; i++ ) - sb.append(" "); - sb.append(" ]] "); - if( _type!= null ) _type.print(sb); - sb.append("\n"); - } - public String p(int depth) { return IRPrinter.prettyPrint(this,depth); } public boolean isMultiHead() { return false; } @@ -165,6 +133,8 @@ public abstract class Node { public boolean isCFG() { return false; } + public boolean isMem() { return false; } + /** * Change a <em>def</em> into a Node. Keeps the edges correct, by removing * the corresponding <em>use->def</em> edge. This may make the original @@ -195,6 +165,7 @@ public abstract class Node { old_def.kill(); // Kill old def // Set the new_def over the old (killed) edge _inputs.set(idx,new_def); + moveDepsToWorklist(); // Return self for easy flow-coding return new_def; } @@ -282,6 +253,8 @@ public abstract class Node { public <N extends Node> N keep() { addUse(null); return (N)this; } // Remove bogus null. public <N extends Node> N unkeep() { delUse(null); return (N)this; } + // Test "keep" status + public boolean iskeep() { return Utils.find(_outputs,null) != -1; } // Replace self with nnn in the graph, making 'this' go dead @@ -337,7 +310,7 @@ public abstract class Node { Type old = setType(compute()); // Replace constant computations from non-constants with a constant node - if (!(this instanceof ConstantNode) && _type.isHighOrConst() ) + if( !(this instanceof ConstantNode) && _type.isHighOrConst() ) return new ConstantNode(_type).peepholeOpt(); // Global Value Numbering @@ -372,7 +345,7 @@ public abstract class Node { private Node deadCodeElim(Node m) { // If self is going dead and not being returned here (Nodes returned // from peephole commonly have no uses (yet)), then kill self. - if( m != this && isUnused() ) { + if( m != this && isUnused() && !isDead() ) { // Killing self - and since self recursively kills self's inputs we // might end up killing 'm', which we are returning as a live Node. // So we add a bogus extra null output edge to stop kill(). @@ -472,9 +445,9 @@ public abstract class Node { public Node dep(int idx) { return _deps.get(idx); } /** - * Add a node to the list o dependencies. Only add it if its not - * an input or output of this node, that is, it is at least one step - * away. The node being added must benefit from this node being peepholed. + * Add a node to the list of dependencies. Only add it if its not an input + * or output of this node, that is, it is at least one step away. The node + * being added must benefit from this node being peepholed. */ Node addDep( Node dep ) { // Running peepholes during the big assert cannot have side effects @@ -563,14 +536,11 @@ public abstract class Node { } /** - * Does this node contain all constants? - * Ignores in(0), as is usually control. - * In an input is not a constant, we add dep as - * a dependency to it, because dep can make progress - * if the input becomes a constant later. - * It is sufficient for one of the non-const - * inputs to have the dependency so we don't bother - * checking the rest. + * Does this node contain all constants? Ignores in(0), as is usually + * control. In an input is not a constant, we add dep as a dependency to + * it because dep can make progress if the input becomes a constant later. + * It is sufficient for one of the non-const inputs to have the dependency, + * so we don't bother checking the rest. */ boolean allCons(Node dep) { for( int i=1; i<nIns(); i++ ) @@ -581,8 +551,15 @@ public abstract class Node { return true; } - // Return the immediate dominator of this Node and compute dom tree depth. - Node idom() { return in(0); } + + /** + * Immediate dominator tree depth, used to approximate a real IDOM depth + * during parsing where we do not have the whole program, and also + * peepholes change the CFG incrementally. + * <p> + * See {@link <a href="https://en.wikipedia.org/wiki/Dominator_(graph_theory)">...</a>} + */ + public char _idepth; // IDOM depth approx; Zero is unset; non-zero is cached legit // Find the lowest common ancestor in the current dominator tree. Node domLCA(Node rhs) { @@ -605,11 +582,17 @@ public abstract class Node { } + // Return the immediate dominator of this Node. + Node idom() { return in(0); } + // Make a shallow copy (same class) of this Node, with given inputs and // empty outputs and a new Node ID. The original inputs are ignored. // Does not need to be implemented in isCFG() nodes. Node copy(Node lhs, Node rhs) { throw Utils.TODO("Binary ops need to implement copy"); } + // Report any post-optimize errors + String err() { return null; } + /** * Used to allow repeating tests in the same JVM. This just resets the * Node unique id generator, and is done as part of making a new Parser. diff --git a/src/main/java/com/seaofnodes/simple/node/NotNode.java b/src/main/java/com/seaofnodes/simple/node/NotNode.java index a9a69c59..83141836 100644 --- a/src/main/java/com/seaofnodes/simple/node/NotNode.java +++ b/src/main/java/com/seaofnodes/simple/node/NotNode.java @@ -1,8 +1,7 @@ package com.seaofnodes.simple.node; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; - +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.*; import java.util.BitSet; public class NotNode extends Node { @@ -18,9 +17,24 @@ public class NotNode extends Node { @Override public Type compute() { - if( in(1)._type instanceof TypeInteger i0 ) + Type t0 = in(1)._type; + switch( t0 ) { + case TypeInteger i0: return i0.isConstant() ? TypeInteger.constant(i0.value()==0 ? 1 : 0) : i0; - return in(1)._type==Type.TOP ? TypeInteger.TOP : TypeInteger.BOT; + case TypeMemPtr p0: + // top->top, bot->bot, null->1, *void->0, not-null ptr->0, ptr/nil->bot + // If input in null then true + // If input is not null ptr then false + if( p0 == TypeMemPtr.TOP ) return TypeInteger.TOP; + if( p0 == TypeMemPtr.NULLPTR ) return TypeInteger.constant(1); + if( !p0._nil ) return TypeInteger.constant(0); + return TypeInteger.BOT; + case Type t: + if( t0.getClass() != Type.class ) + // Only doing NOT on ints and ptrs + throw Utils.TODO(); + return t0==Type.TOP ? TypeInteger.TOP : TypeInteger.BOT; + } } @Override diff --git a/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/src/main/java/com/seaofnodes/simple/node/PhiNode.java index 13c9423d..bdf50847 100644 --- a/src/main/java/com/seaofnodes/simple/node/PhiNode.java +++ b/src/main/java/com/seaofnodes/simple/node/PhiNode.java @@ -1,6 +1,8 @@ package com.seaofnodes.simple.node; import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.Utils; import java.util.BitSet; @@ -8,7 +10,11 @@ public class PhiNode extends Node { final String _label; - public PhiNode(String label, Node... inputs) { super(inputs); _label = label; } + // The Phi type we compute must stay within the domain of the Phi. + // Example Int stays Int, Ptr stays Ptr, Control stays Control, Mem stays Mem. + final Type _declaredType; + + public PhiNode(String label, Type declaredType, Node... inputs) { super(inputs); _label = label; assert declaredType!=null; _declaredType = declaredType; } @Override public String label() { return "Phi_"+_label; } @@ -29,17 +35,21 @@ public class PhiNode extends Node { Node region() { return in(0); } @Override public boolean isMultiTail() { return true; } + @Override + public boolean isMem() { return _declaredType instanceof TypeMem; } @Override public Type compute() { if( !(region() instanceof RegionNode r) ) return region()._type==Type.XCONTROL ? Type.TOP : _type; - if( r.inProgress() ) return Type.BOTTOM; - Type t = Type.TOP; + // During parsing Phis have to be computed type pessimistically. + if( r.inProgress() ) return _declaredType; + // Set type to local top of the starting type + Type t = _declaredType.glb().dual(); for (int i = 1; i < nIns(); i++) // If the region's control input is live, add this as a dependency // to the control because we can be peeped should it become dead. - if( r.in(i).addDep(this)._type != Type.XCONTROL && in(i) != this ) + if( r.in(i).addDep(this)._type != Type.XCONTROL ) t = t.meet(in(i)._type); return t; } @@ -69,11 +79,35 @@ public class PhiNode extends Node { lhss[i] = in(i).in(1); rhss[i] = in(i).in(2); } - Node phi_lhs = new PhiNode(_label,lhss).peephole(); - Node phi_rhs = new PhiNode(_label,rhss).peephole(); + Node phi_lhs = new PhiNode(_label, _declaredType,lhss).peephole(); + Node phi_rhs = new PhiNode(_label, _declaredType,rhss).peephole(); return op.copy(phi_lhs,phi_rhs); } + // If merging Phi(N, cast(N)) - we are losing the cast JOIN effects, so just remove. + if( nIns()==3 ) { + if( in(1) instanceof CastNode cast && cast.in(1).addDep(this)==in(2) ) return in(2); + if( in(2) instanceof CastNode cast && cast.in(1).addDep(this)==in(1) ) return in(1); + } + // If merging a null-checked null and the checked value, just use the value. + // if( val ) ..; phi(Region,False=0/null,True=val); + // then replace with plain val. + if( nIns()==3 ) { + int nullx = -1; + if( in(1)._type == in(1)._type.makeInit() ) nullx = 1; + if( in(2)._type == in(2)._type.makeInit() ) nullx = 2; + if( nullx != -1 ) { + Node val = in(3-nullx); + if( region().idom() instanceof IfNode iff && iff.pred().addDep(this)==val ) { + // Must walk the idom on the null side to make sure we hit False. + Node idom = region().in(nullx); + while( idom.in(0) != iff ) idom = idom.idom(); + if( idom instanceof ProjNode proj && proj._idx==1 ) + return val; + } + } + } + return null; } diff --git a/src/main/java/com/seaofnodes/simple/node/ProjNode.java b/src/main/java/com/seaofnodes/simple/node/ProjNode.java index 0ed9441d..735fcdf3 100644 --- a/src/main/java/com/seaofnodes/simple/node/ProjNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ProjNode.java @@ -1,6 +1,7 @@ package com.seaofnodes.simple.node; import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; import com.seaofnodes.simple.type.TypeTuple; import java.util.BitSet; @@ -27,6 +28,7 @@ public class ProjNode extends Node { @Override public boolean isCFG() { return _idx==0 || ctrl() instanceof IfNode; } @Override public boolean isMultiTail() { return in(0).isMultiHead(); } + @Override public boolean isMem() { return _type instanceof TypeMem; } public MultiNode ctrl() { return (MultiNode)in(0); } @@ -38,8 +40,14 @@ public class ProjNode extends Node { @Override public Node idealize() { - if( ctrl()._type instanceof TypeTuple tt && tt._types[1-_idx]==Type.XCONTROL ) // Only true for IfNodes - return ctrl().in(0); // We become our input control + if ( ctrl() instanceof IfNode iff ) { + if ( iff._type instanceof TypeTuple tt && tt._types[1 - _idx] == Type.XCONTROL ) + return iff.in(0); // We become our input control + + // Flip a negating if-test, to remove the not + if( iff.pred().addDep(this) instanceof NotNode not ) + return new ProjNode((MultiNode)new IfNode(iff.ctrl(),not.in(1)).peephole(),1-_idx,_idx==0 ? "False" : "True"); + } return null; } diff --git a/src/main/java/com/seaofnodes/simple/node/RegionNode.java b/src/main/java/com/seaofnodes/simple/node/RegionNode.java index 71bef275..f7a56503 100644 --- a/src/main/java/com/seaofnodes/simple/node/RegionNode.java +++ b/src/main/java/com/seaofnodes/simple/node/RegionNode.java @@ -35,6 +35,7 @@ public class RegionNode extends Node { @Override public Node idealize() { if( inProgress() ) return null; + // Delete dead paths into a Region int path = findDeadInput(); if( path != 0 && // Do not delete the entry path of a loop (ok to remove the back @@ -51,15 +52,25 @@ public class RegionNode extends Node { while( nouts != nOuts() ) { nouts = nOuts(); for( int i=0; i<nOuts(); i++ ) - if( out(i) instanceof PhiNode phi && phi.nIns()==nIns() ) + if( out(i) instanceof PhiNode phi && phi.nIns()==nIns() ) { phi.delDef(path); + IterPeeps.addAll(phi._outputs); + } } return isDead() ? new ConstantNode(Type.XCONTROL) : delDef(path); } // If down to a single input, become that input - if( nIns()==2 && !hasPhi() ) { + if( nIns()==2 && !hasPhi() ) return in(1); // Collapse if no Phis; 1-input Phis will collapse on their own - } + + // If a CFG diamond with no merging, delete: "if( pred ) {} else {};" + if( !hasPhi() && // No Phi users, just a control user + in(1) instanceof ProjNode p1 && + in(2) instanceof ProjNode p2 && + p1.in(0)==p2.in(0) && + p1.in(0) instanceof IfNode iff ) + return iff.ctrl(); + return null; } diff --git a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java index 922a3649..f13f3708 100644 --- a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -4,19 +4,11 @@ import com.seaofnodes.simple.type.*; import java.util.BitSet; -/** - * The Return node has two inputs. The first input is a control node and the - * second is the data node that supplies the return value. - * <p> - * In this presentation, Return functions as a Stop node, since multiple <code>return</code> statements are not possible. - * The Stop node will be introduced in Chapter 6 when we implement <code>if</code> statements. - * <p> - * The Return's output is the value from the data node. - */ +/** Keeps the return value and all preceding memory effects alive. */ public class ReturnNode extends Node { - public ReturnNode(Node ctrl, Node data) { - super(ctrl, data); + public ReturnNode(Node ctrl, Node data, ScopeNode scope) { + super(ctrl, data, scope.lookup("$mem")); } public Node ctrl() { return in(0); } @@ -27,7 +19,9 @@ public class ReturnNode extends Node { @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { - return expr()._print0(sb.append("return "), visited).append(";"); + sb.append("return "); + expr()._print0(sb, visited); + return sb.append(";"); } @Override public boolean isCFG() { return true; } diff --git a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 3abea54d..455f4b3a 100644 --- a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -2,6 +2,8 @@ package com.seaofnodes.simple.node; import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.TypeMemPtr; import java.util.*; @@ -23,10 +25,16 @@ public class ScopeNode extends Node { */ public final Stack<HashMap<String, Integer>> _scopes; + /** + * Tracks declared types for every name + */ + public Stack<HashMap<String, Type>> _types; + // A new ScopeNode public ScopeNode() { _scopes = new Stack<>(); + _types = new Stack<>(); _type = Type.BOTTOM; } @@ -69,14 +77,15 @@ public class ScopeNode extends Node { @Override public Node idealize() { return null; } - public void push() { _scopes.push(new HashMap<>()); } - public void pop() { popN(_scopes.pop().size()); } + public void push() { _scopes.push(new HashMap<>()); _types.push(new HashMap<>()); } + public void pop() { popN(_scopes.pop().size()); _types.pop(); } /** * Create a new name in the current scope */ - public Node define( String name, Node n ) { + public Node define( String name, Type declaredType, Node n ) { HashMap<String,Integer> syms = _scopes.lastElement(); + _types.lastElement().put(name,declaredType); if( syms.put(name,nIns()) != null ) return null; // Double define return addDef(n); @@ -97,7 +106,7 @@ public class ScopeNode extends Node { public Node update( String name, Node n ) { return update(name,n,_scopes.size()-1); } /** * Both recursive lookup and update. - * + * <p> * A shared implementation allows us to create lazy phis both during * lookups and updates; the lazy phi creation is part of chapter 8. * @@ -120,12 +129,21 @@ public class ScopeNode extends Node { // Set real Phi in the loop head // The phi takes its one input (no backedge yet) from a recursive // lookup, which might have insert a Phi in every loop nest. - : loop.setDef(idx,new PhiNode(name,loop.ctrl(),loop.update(name,null,nestingLevel),null).peephole()); + : loop.setDef(idx,new PhiNode(name, lookupDeclaredType(name),loop.ctrl(),loop.update(name,null,nestingLevel),null).peephole()); setDef(idx,old); } return n==null ? old : setDef(idx,n); // Not lazy, so this is the answer } + // Return declared type + public Type lookupDeclaredType( String name ) { + for( int i=_types.size(); i>0; i-- ) { + Type t = _types.get(i-1).get(name); + if( t != null ) return t; + } + return null; + } + public Node ctrl() { return in(0); } /** @@ -162,8 +180,10 @@ public class ScopeNode extends Node { // 3) Ensure that the order of defs is the same to allow easy merging for( HashMap<String,Integer> syms : _scopes ) dup._scopes.push(new HashMap<>(syms)); + for( HashMap<String,Type> ts : _types ) + dup._types.push(ts); // Types don't change, just keep stacks aligned - dup.addDef(ctrl()); // Control input is just copied + dup.addDef(ctrl()); // Control input is just copied for( int i=1; i<nIns(); i++ ) // For lazy phis on loops we use a sentinel // that will trigger phi creation on update @@ -187,8 +207,9 @@ public class ScopeNode extends Node { if( in(i) != that.in(i) ) // No need for redundant Phis // If we are in lazy phi mode we need to a lookup // by name as it will trigger a phi creation - setDef(i, new PhiNode(ns[i], r, this.lookup(ns[i]), that.lookup(ns[i])).peephole()); + setDef(i, new PhiNode(ns[i], this.lookupDeclaredType(ns[i]), r, this.lookup(ns[i]), that.lookup(ns[i])).peephole()); that.kill(); // Kill merged scope + IterPeeps.add(r); return r.unkeep().peephole(); } @@ -216,11 +237,68 @@ public class ScopeNode extends Node { IterPeeps.addAll(phi._outputs); phi.moveDepsToWorklist(); if( in != phi ) { - phi.subsume(in); + if( !phi.iskeep() ) // Keeping phi around for parser elsewhere + phi.subsume(in); setDef(i,in); // Set the update back into Scope } } } } + + + // Up-casting: using the results of an If to improve a value. + // E.g. "if( ptr ) ptr.field;" is legal because ptr is known not-null. + + // This Scope looks for direct variable uses, or certain simple + // combinations, and replaces the variable with the upcast variant. + public Node upcast( Node ctrl, Node pred, boolean invert ) { + if( ctrl._type==Type.XCONTROL ) return null; + // A single negation is handled below. For !!p (including p != null), + // also refine the underlying p without changing the Boolean result. + if( pred instanceof NotNode not && not.in(1) instanceof NotNode ) { + pred.keep(); // Recursive peepholes may rediscover this Not. + upcast(ctrl,not.in(1),!invert); + pred.unkeep(); + } + // Invert the If conditional + if( invert ) + pred = pred instanceof NotNode not ? not.in(1) : IterPeeps.add(new NotNode(pred).peephole()); + + // Direct use of a value as predicate. This is a zero/null test. + if( Utils.find(_inputs, pred) != -1 ) { + if( !(pred._type instanceof TypeMemPtr tmp) ) + // Must be an `int`, since int and ptr are the only two value types + // being tested. No representation for a generic not-null int, so no upcast. + return null; + if( tmp.isa(TypeMemPtr.VOIDPTR) ) + return null; // Already not-null, no reason to upcast + // Upcast the ptr to not-null ptr, and replace in scope + return replace(pred,new CastNode(TypeMemPtr.VOIDPTR,ctrl,pred).peephole()); + } + + if( pred instanceof NotNode not ) { + // Direct use of a !value as predicate. This is a zero/null test. + if( Utils.find(_inputs, not.in(1)) != -1 ) { + Type tinit = not.in(1)._type.makeInit(); + if( not.in(1)._type.isa(tinit) ) return null; // Already zero/null, no reason to upcast + return replace(not.in(1), new ConstantNode(tinit).peephole()); + } + } + // Apr/9/2024: Attempted to replace X with Y if guarded by a test of + // X==Y. This didn't seem to help very much, or at least in the test + // cases seen so far was a very minor help. + + // No upcast + return null; + } + + private Node replace( Node old, Node cast ) { + assert old!=null && old!=cast; + for( int i=0; i<nIns(); i++ ) + if( in(i)==old ) + setDef(i,cast); + return cast; + } + } diff --git a/src/main/java/com/seaofnodes/simple/node/StartNode.java b/src/main/java/com/seaofnodes/simple/node/StartNode.java index 1e3559ce..1ca52af0 100644 --- a/src/main/java/com/seaofnodes/simple/node/StartNode.java +++ b/src/main/java/com/seaofnodes/simple/node/StartNode.java @@ -1,25 +1,15 @@ package com.seaofnodes.simple.node; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeTuple; - +import com.seaofnodes.simple.type.*; import java.util.BitSet; -/** - * The Start node represents the start of the function. For now, we do not - * have any inputs to Start because our function does not yet accept - * parameters. When we add parameters the value of Start will be a tuple, and - * will require Projections to extract the values. We discuss this in detail - * in Chapter 9: Functions and Calls. - */ +/** The entry tuple: control, initial whole-memory state, and argument. */ public class StartNode extends MultiNode { - final TypeTuple _args; public StartNode(Type[] args) { super(); - _args = TypeTuple.make(args); - _type = _args; + _type = _args = TypeTuple.make(args); } @Override @@ -40,8 +30,6 @@ public class StartNode extends MultiNode { public Node idealize() { return null; } // No immediate dominator, and idepth==0 - @Override - Node idom() { return null; } @Override int idepth() { return 0; } - + @Override Node idom() { return null; } } diff --git a/src/main/java/com/seaofnodes/simple/node/StopNode.java b/src/main/java/com/seaofnodes/simple/node/StopNode.java index 6ed80df4..cb4126f5 100644 --- a/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.Parser; + import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.IterPeeps; @@ -51,9 +53,22 @@ public class StopNode extends Node { return null; } + @Override public Node idom() { return null; } + public Node addReturn(Node node) { return addDef(node); } - public StopNode iterate() { return IterPeeps.iterate(this); } + public StopNode iterate() { return IterPeeps.iterate(this).typeCheck(); } + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); + if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); + return this; + } } diff --git a/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/src/main/java/com/seaofnodes/simple/node/StoreNode.java new file mode 100644 index 00000000..4bc67c3c --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -0,0 +1,66 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.*; + +import java.util.BitSet; + +/** + * Store represents setting a value to a memory based object, in chapter 10 + * this means a field inside a struct. + */ +public class StoreNode extends MemOpNode { + + /** + * @param name The struct field we are assigning to + * @param memSlice The whole-memory node - this is updated after a Store + * @param memPtr The ptr to the struct where we will store a value + * @param value Value to be stored + */ + public StoreNode(String name, Node memSlice, Node memPtr, Node value) { + super(name, memSlice, memPtr, value); + } + + @Override + public String label() { return "Store"; } + @Override + public boolean isMem() { return true; } + public Node val() { return in(3); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return sb.append(".").append(_name).append("=").append( val()).append(";"); + } + + @Override + public Type compute() { return TypeMem.BOT; } + + @Override + public Node idealize() { + + // Simple store-after-store on same address. Should pick up the + // required init-store being stomped by a first user store. + if( mem() instanceof StoreNode st && + ptr()==st.ptr() && _name.equals(st._name) && // Must check same object + ptr()._type instanceof TypeMemPtr && // No bother if weird dead pointers + // Must have exactly one use of "this" or you get weird + // non-serializable memory effects in the worse case. + st.checkNoUseBeyond(this) ) { + setDef(1,st.mem()); + return this; + } + + return null; + } + + // Check that `this` has no uses beyond `that` + private boolean checkNoUseBeyond(Node that) { + if( nOuts()==1 ) return true; + // Add deps on the other uses (can be e.g. ScopeNode mid-parse) so that + // when the other uses go away we can retry. + for( Node use : _outputs ) + if( use != that ) + use.addDep(that); + return false; + } + +} diff --git a/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index df46cc6b..5e21c852 100644 --- a/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -5,9 +5,10 @@ import com.seaofnodes.graph.GraphSnapshot; import com.seaofnodes.graph.GraphSnapshot.*; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMem; import java.util.ArrayList; -/** Chapter 9's view of the IR; browser, transport and layout are shared. */ +/** Chapter 10's view of the IR; browser, transport and layout are shared. */ public class SimpleGraphAdapter extends GraphAdapter<Node> { @Override protected boolean dead(Node n) { return n.isDead(); } @Override protected int id(Node n) { return n._nid; } @@ -42,15 +43,20 @@ public class SimpleGraphAdapter extends GraphAdapter<Node> { if( n instanceof LoopNode ) return Kind.LOOP; if( n instanceof RegionNode ) return Kind.REGION; if( n instanceof PhiNode ) return Kind.PHI; - return n.isCFG() ? Kind.CTRL : Kind.DATA; + return n.isCFG() ? Kind.CTRL : isMem(n) ? Kind.MEM : Kind.DATA; } private Role role(Node n, int i) { if( n instanceof ScopeNode || n instanceof ConstantNode ) return Role.ASSOC; - if( n instanceof PhiNode ) return i == 0 ? Role.ASSOC : Role.DATA; - if( n instanceof ProjNode ) return n.isCFG() ? Role.CTRL : Role.DATA; + if( n instanceof PhiNode ) return i == 0 ? Role.ASSOC : isMem(n) ? Role.MEM : Role.DATA; + if( n instanceof ProjNode ) return n.isCFG() ? Role.CTRL : isMem(n) ? Role.MEM : Role.DATA; if( n instanceof RegionNode && i == 0 ) return Role.ASSOC; if( i == 0 || n instanceof RegionNode || n instanceof StopNode ) return Role.CTRL; + if( i == 1 && n instanceof MemOpNode || i == 2 && n instanceof ReturnNode ) return Role.MEM; + Node def = n.in(i); + if( def != null && isMem(def) ) return Role.MEM; return Role.DATA; } + + private boolean isMem(Node n) { return n.isMem() || n._type instanceof TypeMem; } } diff --git a/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java index 2c6c90ca..1980659e 100644 --- a/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -7,7 +7,7 @@ import com.seaofnodes.simple.node.Node; import java.util.ArrayList; import java.io.IOException; -/** Chapter 9's compilation context; capture and event bookkeeping are shared. */ +/** Chapter 10's compilation context; capture and event bookkeeping are shared. */ public class SimpleGraphObserver extends GraphCapture<Node> { private Parser _parser; private Node _ret; @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture<Node> { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/src/main/java/com/seaofnodes/simple/type/Field.java b/src/main/java/com/seaofnodes/simple/type/Field.java new file mode 100644 index 00000000..cdc58215 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/type/Field.java @@ -0,0 +1,59 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a field in a struct. This is not a Type in the type system. + */ +public class Field extends Type { + + // The pair {fieldName,type} uniquely identifies a field. + + // Field name + public final String _fname; + // Type of the field + public final Type _type; + + private Field(String fname, Type type ) { + super(TFLD); + _fname = fname; + _type = type; + } + public static Field make( String fname, Type type ) { + return new Field(fname,type).intern(); + } + + public static final Field TEST = make("test",TypeInteger.ZERO); + public static void gather(ArrayList<Type> ts) { ts.add(TEST); } + + @Override Field xmeet( Type that ) { + Field fld = (Field)that; // Invariant + assert _fname.equals(fld._fname); + return make(_fname,_type.meet(fld._type)); + } + + @Override + public Field dual() { return make(_fname,_type.dual()); } + + @Override + public Field glb() { + return make(_fname,_type.glb()); + } + + // Override in subclasses + int hash() { return _fname.hashCode() ^ _type.hashCode(); } + + boolean eq(Type t) { + Field f = (Field)t; + return Utils.eq(_fname,f._fname) && _type==f._type; + } + + + @Override + public StringBuilder print( StringBuilder sb ) { + return _type.print(sb.append(_fname).append(":")); + } + + @Override public String str() { return _fname; } +} diff --git a/src/main/java/com/seaofnodes/simple/type/Type.java b/src/main/java/com/seaofnodes/simple/type/Type.java index daaa8af6..bd9cf458 100644 --- a/src/main/java/com/seaofnodes/simple/type/Type.java +++ b/src/main/java/com/seaofnodes/simple/type/Type.java @@ -2,6 +2,7 @@ package com.seaofnodes.simple.type; import com.seaofnodes.simple.Utils; +import java.util.ArrayList; import java.util.HashMap; /** @@ -33,6 +34,10 @@ public class Type { static final byte TSIMPLE = 4; // End of the Simple Types static final byte TINT = 5; // All Integers; see TypeInteger static final byte TTUPLE = 6; // Tuples; finite collections of unrelated Types, kept in parallel + static final byte TMEM = 7; // Whole-memory token + static final byte TMEMPTR = 8; // Memory pointer type + static final byte TSTRUCT = 9; // Structs; tuples with named fields + static final byte TFLD =10; // Fields into struct public final byte _type; @@ -44,6 +49,21 @@ public class Type { public static final Type TOP = new Type( TTOP ).intern(); // ANY public static final Type CONTROL = new Type( TCTRL ).intern(); // Ctrl public static final Type XCONTROL = new Type( TXCTRL ).intern(); // ~Ctrl + public static Type[] gather() { + ArrayList<Type> ts = new ArrayList<>(); + ts.add(BOTTOM); + ts.add(CONTROL); + Field.gather(ts); + TypeInteger.gather(ts); + TypeMem.gather(ts); + TypeMemPtr.gather(ts); + TypeStruct.gather(ts); + TypeTuple.gather(ts); + int sz = ts.size(); + for( int i = 0; i < sz; i++ ) + ts.add(ts.get(i).dual()); + return ts.toArray(new Type[ts.size()]); + } // Is high or on the lattice centerline. public boolean isHighOrConst() { return _type==TTOP || _type==TXCTRL; } @@ -52,12 +72,26 @@ public class Type { // Excludes both high and low values public boolean isConstant() { return false; } - public StringBuilder print(StringBuilder sb) {return is_simple() ? sb.append(STRS[_type]) : sb;} + /** + * Display Type name in a format that's good for IR printer + */ + public StringBuilder typeName( StringBuilder sb) { return print(sb); } + + public Type makeInit() { return null; } // ---------------------------------------------------------- + // Notes on Type interning. + // At the moment it is not easy to reset the interned types + // because we hold static references to several types and these are scattered + // around. This means the INTERN cache will retain all types from + // every run of the Parser. For this to work correctly types must be + // rigorous about defining when they are the same. Also types need to be + // immutable once defined. + // The rationale for interning is performance. + // Factory method which interns "this" - protected <T extends Type> T intern() { + public <T extends Type> T intern() { T nnn = (T)INTERN.get(this); if( nnn==null ) INTERN.put(nnn=(T)this,this); @@ -95,13 +129,13 @@ public class Type { // Reverse; xmeet 2nd arg is never "is_simple" and never equal to "this". if( is_simple() ) return this.xmeet(t ); if( t.is_simple() ) return t .xmeet(this); - return xmeet(t); // Mixing 2 unrelated types + return Type.BOTTOM; // Mixing 2 unrelated types } // Compute meet right now. Overridden in subclasses. // Handle cases where 'this.is_simple()' and unequal to 't'. // Subclassed xmeet calls can assert that '!t.is_simple()'. - protected Type xmeet(Type t) { + Type xmeet(Type t) { assert is_simple(); // Should be overridden in subclass // ANY meet anything is thing; thing meet ALL is ALL if( _type==TBOT || t._type==TTOP ) return this; @@ -112,17 +146,6 @@ public class Type { return _type==TCTRL || t._type==TCTRL ? CONTROL : XCONTROL; } - // True if this "isa" t; e.g. 17 isa TypeInteger.BOT - public boolean isa( Type t ) { return meet(t)==t; } - - // ---------------------------------------------------------- - // Our lattice is defined with a MEET and a DUAL. - // JOIN is dual of meet of both duals. - public final Type join(Type t) { - if( this==t ) return this; - return dual().meet(t.dual()).dual(); - } - public Type dual() { return switch( _type ) { case TBOT -> TOP; @@ -134,8 +157,31 @@ public class Type { } // ---------------------------------------------------------- + // Our lattice is defined with a MEET and a DUAL. + // JOIN is dual of meet of both duals. + public final Type join(Type t) { + if( this==t ) return this; + return dual().meet(t.dual()).dual(); + } + + // True if this "isa" t; e.g. 17 isa TypeInteger.BOT + public boolean isa( Type t ) { return meet(t)==t; } + + /** + * Compute greatest lower bound in the lattice + */ + public Type glb() { return _type==TCTRL ? XCONTROL : BOTTOM; } + + // ---------------------------------------------------------- + // Useful in the debugger, which calls toString everywhere. + // This is a more verbose dev-friendly print. @Override public final String toString() { return print(new StringBuilder()).toString(); } + + public StringBuilder print(StringBuilder sb) { return is_simple() ? sb.append(STRS[_type]) : sb;} + + // This is used by error messages, and is a shorted print. + public String str() { return STRS[_type]; } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java index ab9dbcff..8c77672b 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.type; +import java.util.ArrayList; + /** * Integer Type */ @@ -28,6 +30,10 @@ public class TypeInteger extends Type { public static TypeInteger constant(long con) { return make(true, con); } + public static void gather(ArrayList<Type> ts) { ts.add(ZERO); ts.add(BOT); } + + // FIXME this display format is problematic + // In visualizer '#' gets prepended if its a constant @Override public StringBuilder print(StringBuilder sb) { if( this==TOP ) return sb.append("IntTop"); @@ -35,6 +41,22 @@ public class TypeInteger extends Type { return sb.append(_con); } + @Override public String str() { + if( this==TOP ) return "~int"; + if( this==BOT ) return "int"; + return ""+_con; + } + + /** + * Display Type name in a format that's good for IR printer + */ + @Override + public StringBuilder typeName(StringBuilder sb) { + if( this==TOP ) return sb.append("IntTop"); + if( this==BOT ) return sb.append("IntBot"); + return sb.append("Int"); + } + @Override public boolean isHighOrConst() { return _is_con || _con==0; } @@ -65,6 +87,11 @@ public class TypeInteger extends Type { } @Override + public Type glb() { return BOT; } + + @Override public TypeInteger makeInit() { return ZERO; } + + @Override int hash() { return (int)(_con ^ (_is_con ? 0 : 0x4000)); } @Override public boolean eq( Type t ) { diff --git a/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/src/main/java/com/seaofnodes/simple/type/TypeMem.java new file mode 100644 index 00000000..d921bac3 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/type/TypeMem.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +/** A token for all memory; field values are not tracked in its type. */ +public class TypeMem extends Type { + private final boolean _high; + private TypeMem(boolean high) { super(TMEM); _high = high; } + public static final TypeMem TOP = new TypeMem(true ).intern(); + public static final TypeMem BOT = new TypeMem(false).intern(); + + public static void gather(ArrayList<Type> ts) { ts.add(BOT); } + @Override TypeMem xmeet(Type t) { return BOT; } + @Override public Type dual() { return _high ? BOT : TOP; } + @Override public Type glb() { return BOT; } + @Override int hash() { return _high ? 1 : 2; } + @Override boolean eq(Type t) { return _high==((TypeMem)t)._high; } + @Override public StringBuilder print(StringBuilder sb) { return sb.append(_high ? "MemTop" : "MemBot"); } + @Override public String str() { return print(new StringBuilder()).toString(); } +} diff --git a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java new file mode 100644 index 00000000..6c874a93 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java @@ -0,0 +1,82 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +/** + * Represents a Pointer to memory. + * + * Null is generic pointer to non-existent memory. + * *void is a non-Null pointer to all possible structs. + * Pointers can be to specific struct types, or a union with Null. + * The distinguished *$BOT ptr represents union of *void and Null. + * The distinguished *$TOP ptr represents the dual of *$BOT. + */ +public class TypeMemPtr extends Type { + // A TypeMemPtr is pair (obj,nil) + // where obj is one of + // (null,TypeStruct). + // where nil is one of + // (true,false) meaning an explicit null is allowed or not + + public final TypeStruct _obj; // null, a TypeStruct, or sentinel TypeStruct.MANY + public final boolean _nil; + + private TypeMemPtr(TypeStruct obj, boolean nil) { + super(TMEMPTR); + _obj = obj; + _nil = nil; + } + public static TypeMemPtr make(TypeStruct obj, boolean nil) { return new TypeMemPtr(obj, nil).intern(); } + public static TypeMemPtr make(TypeStruct obj) { return TypeMemPtr.make(obj, false); } + + public static final TypeMemPtr BOT = make(TypeStruct.BOT,true); + public static final TypeMemPtr TOP = BOT.dual(); + public static final TypeMemPtr NULLPTR = make(TypeStruct.TOP,true); + public static final TypeMemPtr VOIDPTR = NULLPTR.dual(); // A bottom mix of not-null ptrs, like C's void* but not null + public static final TypeMemPtr TEST= make(TypeStruct.TEST,false); + public static void gather(ArrayList<Type> ts) { ts.add(NULLPTR); ts.add(BOT); ts.add(TEST); } + + @Override + Type xmeet(Type t) { + TypeMemPtr that = (TypeMemPtr) t; + return TypeMemPtr.make((TypeStruct)_obj.meet(that._obj), _nil | that._nil); + } + + @Override + public TypeMemPtr dual() { return TypeMemPtr.make(_obj==null ? null : _obj.dual(), !_nil); } + + @Override + public Type glb() { + if( _obj==null ) return BOT; + return make(_obj.glb(),true); + } + @Override public TypeMemPtr makeInit() { return NULLPTR; } + + @Override + int hash() { return (_obj==null ? 0xDEADBEEF : _obj.hashCode()) ^ (_nil ? 1024 : 0); } + + @Override + boolean eq(Type t) { + TypeMemPtr ptr = (TypeMemPtr)t; // Invariant + return _obj == ptr._obj && _nil == ptr._nil; + } + + // [void,name,MANY]*[,?] + @Override + public StringBuilder print(StringBuilder sb) { + if( this==BOT ) return sb.append("PtrBot"); + if( this==TOP ) return sb.append("PtrTop"); + if( this== NULLPTR) return sb.append("null"); + if( this== VOIDPTR) return sb.append("*void"); + return _obj.print(sb.append("*")).append(_nil ? "?" : ""); + } + + @Override public String str() { + if( this==BOT ) return "PtrBot"; + if( this==TOP ) return "PtrTop"; + if( this== NULLPTR) return "null"; + if( this== VOIDPTR) return "*void"; + return "*"+_obj.str()+(_nil ? "?" : ""); + } + +} diff --git a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java new file mode 100644 index 00000000..44741def --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -0,0 +1,127 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a struct type. + */ +public class TypeStruct extends Type { + + // For now in terms of the lattice + // a struct type just stays as is + + public final String _name; + public final Field[] _fields; + + private TypeStruct(String name, Field[] fields) { + super(Type.TSTRUCT); + _name = name; + _fields = fields; + } + + public static TypeStruct make(String name, Field[] fields) { return new TypeStruct(name, fields).intern(); } + public static TypeStruct make(String name, ArrayList<Field> fields) { return make(name,fields.toArray(new Field[fields.size()])); } + + public static final TypeStruct TOP = make("$TOP",new Field [0]); + public static final TypeStruct BOT = make("$BOT",new Field [0]); + public static final TypeStruct TEST = make("test",new Field[]{Field.TEST}); + public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(BOT); } + + public int find(String fname) { + for( int i=0; i<_fields.length; i++ ) + if( _fields[i]._fname.equals(fname) ) + return i; + return -1; + } + + @Override + Type xmeet(Type t) { + TypeStruct that = (TypeStruct) t; + if( this==TOP ) return that; + if( that==TOP ) return this; + if( this==BOT ) return BOT; + if( that==BOT ) return BOT; + + // Within the same compilation unit, struct names are unique. If the + // names differ, its different structs. Across many compilation units, + // structs with the same name but different field layouts can be + // interned... which begs the question: + // "What is the meet of structs from two different compilation units?" + // And the answer is: "don't ask". + if( !_name.equals(that._name) ) + return BOT; // It's a struct; that's about all we know + // Now all fields should be the same, so just do field meets + assert _fields.length == that._fields.length; + Field[] flds = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) + flds[i] = (Field)_fields[i].meet(that._fields[i]); + return make(_name,flds); + } + + @Override + public TypeStruct dual() { + if( this==TOP ) return BOT; + if( this==BOT ) return TOP; + Field[] flds = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) + flds[i] = _fields[i].dual(); + return make(_name,flds); + } + + // Keeps the same struct, but lower-bounds all fields. + @Override + public TypeStruct glb() { + if( _glb() ) return this; + // Need to glb each field + Field[] flds = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) + flds[i] = _fields[i].glb(); + return make(_name,flds); + } + private boolean _glb() { + for( Field f : _fields ) + if( f.glb() != f ) + return false; + return true; + } + + @Override + boolean eq(Type t) { + TypeStruct ts = (TypeStruct)t; // Invariant + if( !_name.equals(ts._name) ) + return false; + if( _fields.length!=ts._fields.length ) + return false; + for( int i = 0; i < _fields.length; i++ ) + if( _fields[i] != ts._fields[i] ) + return false; + return true; + } + + @Override + int hash() { + long hash = _name.hashCode(); + for( Field f : _fields ) hash = Utils.rot(hash,13) ^ f.hashCode(); + return Utils.fold(hash); + } + + @Override + public StringBuilder print(StringBuilder sb) { + if( this==BOT ) return sb.append("StructBot"); + if( this==TOP ) return sb.append("StructTop"); + sb.append(_name).append(" {\n"); + for( Field f : _fields ) { + sb.append(" ").append(f._fname).append(":"); + f._type.print(sb); + sb.append(";\n"); + } + return sb.append("}"); + } + + @Override public String str() { + if( this==BOT ) return "StructBot"; + if( this==TOP ) return "StructTop"; + return _name; + } +} diff --git a/src/main/java/com/seaofnodes/simple/type/TypeTuple.java b/src/main/java/com/seaofnodes/simple/type/TypeTuple.java index a2a6a095..0aa019a7 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeTuple.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeTuple.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.type; +import java.util.ArrayList; + public class TypeTuple extends Type { public final Type[] _types; @@ -7,8 +9,11 @@ public class TypeTuple extends Type { private TypeTuple(Type[] types) { super(TTUPLE); _types = types; } public static TypeTuple make(Type... types) { return new TypeTuple(types).intern(); } + private static TypeTuple TEST = make(TypeInteger.BOT,TypeMemPtr.TEST); + public static void gather(ArrayList<Type> ts) { ts.add(TEST); } + @Override - public Type xmeet(Type other) { + Type xmeet(Type other) { TypeTuple tt = (TypeTuple)other; // contract from xmeet assert _types.length == tt._types.length; Type[] ts = new Type[_types.length]; @@ -25,6 +30,14 @@ public class TypeTuple extends Type { } @Override + public Type glb() { + Type[] ts = new Type[_types.length]; + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i].glb(); + return make(ts); + } + + @Override public StringBuilder print(StringBuilder sb) { sb.append("["); for( Type t : _types ) @@ -34,6 +47,21 @@ public class TypeTuple extends Type { return sb; } + @Override public String str() { return print(new StringBuilder()).toString(); } + + /** + * Display Type name in a format that's good for IR printer + */ + @Override + public StringBuilder typeName(StringBuilder sb) { + sb.append("["); + for( Type t : _types ) + t.typeName(sb).append(","); + sb.setLength(sb.length()-1); + sb.append("]"); + return sb; + } + public static final TypeTuple IF_BOTH = make(new Type[]{Type. CONTROL,Type. CONTROL}); public static final TypeTuple IF_NEITHER = make(new Type[]{Type.XCONTROL,Type.XCONTROL}); public static final TypeTuple IF_TRUE = make(new Type[]{Type. CONTROL,Type.XCONTROL}); diff --git a/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/src/test/java/com/seaofnodes/simple/Chapter01Test.java index 13b42f96..89882b2f 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter01Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter01Test.java @@ -41,7 +41,7 @@ public class Chapter01Test { new Parser("ret").parse(); fail(); } catch( RuntimeException e ) { - assertEquals("Syntax error, expected =: ",e.getMessage()); + assertEquals("Undefined name 'ret'",e.getMessage()); } } @@ -83,7 +83,7 @@ public class Chapter01Test { new Parser("return100").parse(); fail(); } catch( RuntimeException e ) { - assertEquals("Syntax error, expected =: ",e.getMessage()); + assertEquals("Undefined name 'return100'",e.getMessage()); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter03Test.java b/src/test/java/com/seaofnodes/simple/Chapter03Test.java index c3cd43c3..609dff76 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter03Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter03Test.java @@ -31,7 +31,7 @@ public class Chapter03Test { @Test public void testVarDist() { - Parser parser = new Parser("int x0=1; int y0=2; int x1=3; int y1=4; return (x0-x1)*(x0-x1) + (y0-y1)*(y0-y1);"); + Parser parser = new Parser("int x0=1; int y0=2; int x1=3; int y1=4; return (x0-x1)*(x0-x1) + (y0-y1)*(y0-y1); "); StopNode ret = parser.parse(); assertEquals("return 8;", ret.print()); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/src/test/java/com/seaofnodes/simple/Chapter05Test.java index 6ad3af15..aefb3a5c 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter05Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter05Test.java @@ -22,7 +22,7 @@ else { return a;"""); StopNode ret = parser.parse().iterate(); - assertEquals("return Phi(Region17,(arg+2),(arg-3));", ret.toString()); + assertEquals("return Phi(Region19,(arg+2),(arg-3));", ret.toString()); } @Test @@ -37,7 +37,7 @@ if (arg == 1) { } return c;""", TypeInteger.BOT); StopNode ret = parser.parse().iterate(); - assertEquals("return Phi(Region16,4,3);", ret.toString()); + assertEquals("return Phi(Region17,4,3);", ret.toString()); } @Test @@ -65,7 +65,7 @@ else b=a+1; return a+b;"""); StopNode ret = parser.parse().iterate(); - assertEquals("return ((arg*2)+Phi(Region20,2,3));", ret.toString()); + assertEquals("return ((arg*2)+Phi(Region22,2,3));", ret.toString()); } @Test @@ -80,7 +80,7 @@ else a=b+1; return a+b;"""); StopNode ret = parser.parse().iterate(); - assertEquals("return ((Phi(Region31,(arg*2),arg)+arg)+Phi(Region,4,5));", ret.toString()); + assertEquals("return ((Phi(Region33,(arg*2),arg)+arg)+Phi(Region,4,5));", ret.toString()); } @Test @@ -100,7 +100,7 @@ else return a; """, TypeInteger.BOT); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region33,Phi(Region21,2,3),Phi(Region31,4,5));", stop.toString()); + assertEquals("return Phi(Region37,Phi(Region23,2,3),Phi(Region35,4,5));", stop.toString()); } @Test @@ -116,7 +116,7 @@ if( arg==0 ) return arg+a+b; """, TypeInteger.BOT); StopNode stop = parser.parse(); - assertEquals("return ((arg+Phi(Region13,1,0))+Phi(Region22,2,0));", stop.toString()); + assertEquals("return ((arg+Phi(Region14,1,0))+Phi(Region29,2,0));", stop.toString()); } @Test @@ -130,7 +130,7 @@ if( arg==1 ) } return a;"""); StopNode ret = parser.parse().iterate(); - assertEquals("return (arg==Phi(Region16,3,2));", ret.toString()); + assertEquals("return (arg==Phi(Region17,3,2));", ret.toString()); } @Test @@ -206,7 +206,7 @@ return a;"""); new Parser("int a=1; ififif(arg)inta=2;return a;").parse(); fail(); } catch( RuntimeException e ) { - assertEquals("Syntax error, expected =: (",e.getMessage()); + assertEquals("Undefined name 'ififif'",e.getMessage()); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter06Test.java b/src/test/java/com/seaofnodes/simple/Chapter06Test.java index 054e96e8..920e26e3 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter06Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter06Test.java @@ -30,7 +30,7 @@ if (arg) return (arg < a) < 3; """); StopNode stop = parser.parse().iterate(); - assertEquals("return ((arg<Phi(Region12,2,1))<3);", stop.toString()); + assertEquals("return ((arg<Phi(Region13,2,1))<3);", stop.toString()); } @Test @@ -65,7 +65,7 @@ else b=5; return b;""", TypeInteger.BOT); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region32,42,5);", stop.toString()); + assertEquals("return Phi(Region38,42,5);", stop.toString()); } @Test @@ -84,7 +84,7 @@ else b=5; return b;""", TypeInteger.BOT); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region29,2,5);", stop.toString()); + assertEquals("return Phi(Region31,2,5);", stop.toString()); } @Test @@ -144,7 +144,7 @@ else return a; """, TypeInteger.BOT); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region37,3,Phi(Region35,4,5));", stop.toString()); + assertEquals("return Phi(Region42,3,Phi(Region40,4,5));", stop.toString()); } @Test @@ -200,7 +200,7 @@ if( arg ) { return a+b; """); StopNode ret = parser.parse().iterate(); - assertEquals("return Phi(Region24,4,1);", ret.toString()); + assertEquals("return Phi(Region25,4,1);", ret.toString()); } @@ -251,7 +251,7 @@ if( arg ) { return a+b+c; """); StopNode ret = parser.parse().iterate(); - assertEquals("return (Phi(Region35,Phi(Region22,2,3),0)+Phi(Region,3,1));", ret.toString()); + assertEquals("return (Phi(Region37,Phi(Region24,2,3),0)+Phi(Region,3,1));", ret.toString()); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter07Test.java b/src/test/java/com/seaofnodes/simple/Chapter07Test.java index 109ee830..ff0ca599 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter07Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter07Test.java @@ -18,7 +18,7 @@ public class Chapter07Test { return arg; """); StopNode stop = parser.parse(); - assertEquals("return Phi(Loop6,arg,(Phi_arg+1));", stop.toString()); + assertEquals("return Phi(Loop7,arg,(Phi_arg+1));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -35,7 +35,7 @@ if(arg){}else{ return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region22,1,Phi(Loop11,1,(Phi_a+1)));", stop.toString()); + assertEquals("return Phi(Region25,1,Phi(Loop14,1,(Phi_a+1)));", stop.toString()); } @Test @@ -55,7 +55,7 @@ while(i < arg) { return sum; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop8,0,Phi(Loop20,Phi_sum,(Phi_sum+Phi(Loop,0,(Phi_j+1)))));", stop.toString()); + assertEquals("return Phi(Loop9,0,Phi(Loop21,Phi_sum,(Phi_sum+Phi(Loop,0,(Phi_j+1)))));", stop.toString()); } @Test @@ -71,7 +71,7 @@ while(a < 10) { return b; """); StopNode stop = parser.parse(); - assertEquals("return Phi(Loop8,2,Phi(Region26,Phi_b,4));", stop.toString()); + assertEquals("return Phi(Loop9,2,Phi(Region28,Phi_b,4));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -90,7 +90,7 @@ while(a < 10) { return b; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop8,2,(Phi(Region26,Phi_b,4)+1));", stop.toString()); + assertEquals("return Phi(Loop9,2,(Phi(Region28,Phi_b,4)+1));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -107,7 +107,7 @@ while(a < 10) { return a; """); StopNode stop = parser.parse(); - assertEquals("return Phi(Loop7,1,(Phi_a+3));", stop.toString()); + assertEquals("return Phi(Loop8,1,(Phi_a+3));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -123,7 +123,7 @@ while(a < 10) { return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop7,1,(Phi_a+3));", stop.toString()); + assertEquals("return Phi(Loop8,1,(Phi_a+3));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -136,7 +136,7 @@ while(arg) a = 2; return a; """); StopNode stop = parser.parse(); - assertEquals("return Phi(Loop7,1,2);", stop.toString()); + assertEquals("return Phi(Loop8,1,2);", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -149,7 +149,7 @@ while(arg) a = 2; return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop7,1,2);", stop.toString()); + assertEquals("return Phi(Loop8,1,2);", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -165,7 +165,7 @@ while(a < 10) { return a; """); StopNode stop = parser.parse(); - assertEquals("return Phi(Loop7,1,(Phi_a+3));", stop.toString()); + assertEquals("return Phi(Loop8,1,(Phi_a+3));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -181,7 +181,7 @@ while(a < 10) { return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop7,1,(Phi_a+3));", stop.toString()); + assertEquals("return Phi(Loop8,1,(Phi_a+3));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -198,7 +198,7 @@ while(a < 10) { return a; """); StopNode stop = parser.parse(); - assertEquals("return Phi(Loop8,1,(Phi_a+3));", stop.toString()); + assertEquals("return Phi(Loop9,1,(Phi_a+3));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -215,7 +215,7 @@ while(a < 10) { return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop8,1,(Phi_a+3));", stop.toString()); + assertEquals("return Phi(Loop9,1,(Phi_a+3));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter08Test.java b/src/test/java/com/seaofnodes/simple/Chapter08Test.java index cea6dd0f..4fa08bd9 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter08Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter08Test.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.evaluator.Evaluator; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeInteger; import org.junit.Assert; @@ -23,10 +24,10 @@ while(arg < 10) { return arg; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region36,Phi(Region25,Phi(Loop6,arg,(Phi_arg+1)),Add),Add);", stop.toString()); + assertEquals("return Phi(Region37,Phi(Region26,Phi(Loop7,arg,(Phi_arg+1)),Add),Add);", stop.toString()); assertTrue(stop.ret().ctrl() instanceof RegionNode); - Assert.assertEquals(5, GraphEvaluator.evaluate(stop, 1)); - Assert.assertEquals(10, GraphEvaluator.evaluate(stop, 6)); + Assert.assertEquals(5L, Evaluator.evaluate(stop, 1)); + Assert.assertEquals(10L, Evaluator.evaluate(stop, 6)); } @@ -46,7 +47,7 @@ while(arg < 10) { return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop7,1,Phi(Region42,Phi_a,(Phi_a+1)));", stop.toString()); + assertEquals("return Phi(Loop8,1,Phi(Region43,Phi_a,(Phi_a+1)));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -65,7 +66,7 @@ while(arg < 10) { return arg; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region34,Phi(Loop6,arg,(Phi_arg+1)),Add);", stop.toString()); + assertEquals("return Phi(Region35,Phi(Loop7,arg,(Phi_arg+1)),Add);", stop.toString()); assertTrue(stop.ret().ctrl() instanceof RegionNode); } @@ -81,7 +82,7 @@ while(arg < 10) { return arg; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region25,Phi(Loop6,arg,(Phi_arg+1)),Add);", stop.toString()); + assertEquals("return Phi(Region26,Phi(Loop7,arg,(Phi_arg+1)),Add);", stop.toString()); assertTrue(stop.ret().ctrl() instanceof RegionNode); } @@ -100,7 +101,7 @@ while(arg < 10) { return arg; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop6,arg,(Phi_arg+1));", stop.toString()); + assertEquals("return Phi(Loop7,arg,(Phi_arg+1));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -117,7 +118,7 @@ while(arg < 10) { return arg; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop6,arg,(Phi_arg+1));", stop.toString()); + assertEquals("return Phi(Loop7,arg,(Phi_arg+1));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); } @@ -185,7 +186,7 @@ while(arg < 10) { return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region28,Phi(Loop7,1,(Phi_a+1)),Add);", stop.toString()); + assertEquals("return Phi(Region29,Phi(Loop8,1,(Phi_a+1)),Add);", stop.toString()); assertTrue(stop.ret().ctrl() instanceof RegionNode); } @@ -201,7 +202,7 @@ while(1) { return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return (Phi(Loop7,1,Add)+1);", stop.toString()); + assertEquals("return (Phi(Loop8,1,Add)+1);", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode prj && prj._idx==1 ); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/src/test/java/com/seaofnodes/simple/Chapter09Test.java index 8103b903..1b1920e8 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter09Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter09Test.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.evaluator.Evaluator; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeInteger; @@ -43,8 +44,8 @@ return x; """); StopNode stop = parser.parse(); assertEquals("Stop[ return (arg*2); return (Mul+1); ]", stop.toString()); - Assert.assertEquals(2, GraphEvaluator.evaluate(stop, 1)); - Assert.assertEquals(23, GraphEvaluator.evaluate(stop, 11)); + Assert.assertEquals(2L, Evaluator.evaluate(stop, 1)); + Assert.assertEquals(23L, Evaluator.evaluate(stop, 11)); } @Test @@ -55,7 +56,7 @@ return arg*arg-arg*arg; """); StopNode stop = parser.parse(); assertEquals("return 0;", stop.toString()); - Assert.assertEquals(0, GraphEvaluator.evaluate(stop, 1)); + Assert.assertEquals(0L, Evaluator.evaluate(stop, 1)); } @Test @@ -69,8 +70,8 @@ while (arg < 10) { return arg; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop7,arg,(Phi_arg+2));", stop.toString()); - Assert.assertEquals(11, GraphEvaluator.evaluate(stop, 1)); + assertEquals("return Phi(Loop8,arg,(Phi_arg+2));", stop.toString()); + Assert.assertEquals(11L, Evaluator.evaluate(stop, 1)); } @Test @@ -86,8 +87,8 @@ while (arg < 10) { return arg; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop8,arg,(Phi_arg+4));", stop.toString()); - Assert.assertEquals(13, GraphEvaluator.evaluate(stop, 1)); + assertEquals("return Phi(Loop9,arg,(Phi_arg+4));", stop.toString()); + Assert.assertEquals(13L, Evaluator.evaluate(stop, 1)); } @Test @@ -115,7 +116,7 @@ while (arg) { return arg; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop14,arg,(Phi_arg+1));", stop.toString()); + assertEquals("return Phi(Loop15,arg,(Phi_arg+1));", stop.toString()); } @Test @@ -137,26 +138,6 @@ while(v1+arg) { } @Test - public void testSccp() { - Parser parser = new Parser( -""" -int x = 1; -int cnt = 5; -while (cnt) { - cnt = cnt - 1; - if (x == 0) { - x = x + 1; - } -} -return x; -""" - ); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop8,1,Phi(Region26,(Phi_x+1),Phi_x));", stop.toString()); - - } - - @Test public void testWhile0() { Parser parser = new Parser("while(0) continue; if(0) arg=0;"); StopNode stop = parser.parse().iterate(); @@ -205,7 +186,7 @@ while(arg) { return a; """); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop9,0,(-(Phi_a+3)));", stop.toString()); + assertEquals("return Phi(Loop10,0,(-(Phi_a+3)));", stop.toString()); } @Test @@ -302,7 +283,7 @@ return v1+v0; public void testFuzz8() { Parser parser = new Parser("while(arg) arg = arg - 1; return arg;"); StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop6,arg,(Phi_arg-1));", stop.toString()); + assertEquals("return Phi(Loop7,arg,(Phi_arg-1));", stop.toString()); } @Test diff --git a/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/src/test/java/com/seaofnodes/simple/Chapter10Test.java new file mode 100644 index 00000000..26bbb62a --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -0,0 +1,522 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; +import com.seaofnodes.simple.type.*; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter10Test { + @Test public void testDeadReferenceReturn() { + for( String body : new String[] { + "return new S; return 0;", + "if(1) return new S; else return 0;" + } ) { + String src = "struct S { int x; }; " + body; + var stop = new Parser(src).parse().iterate(); + org.junit.Assert.assertTrue(body, + stop.ret().expr()._type instanceof com.seaofnodes.simple.type.TypeMemPtr); + } + } + + + + // Issue #246: null-check guards start in Chapter 10; arrays arrive in Chapter 15. + private static final String NULLABLE_POINT_SOURCE = """ + struct Point { int x; }; + Point point = new Point; + point.x = 42; + Point? p = null; + if (arg) p = point; + """; + + @Test + public void testNullGuards() { + for( String body : new String[] { + "if (p != null) return p.x; return -1;", + "if (null != p) return p.x; return -1;", + "if (!!!!p) return p.x; return -1;", + "if (!!!p) return -1; else return p.x;", + "int b = !!p; if (b) return p.x + b - 1; return -1;" + } ) { + StopNode stop = new Parser(NULLABLE_POINT_SOURCE+body).parse().iterate(); + assertEquals(body,-1L,com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,0)); + assertEquals(body,42L,com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,1)); + } + } + + @Test + public void testNullGuardErrors() { + for( String body : new String[] { + "return p.x;", + "if (p == null) return p.x; return -1;", + "if (!!point) return p.x; return -1;", + "if (!!p) { int x = p.x; } return p.x;" + } ) { + try { + new Parser(NULLABLE_POINT_SOURCE+body).parse().iterate(); + fail(body); + } catch( RuntimeException e ) { + // Known null is rejected during parsing in these chapters. + String expected = body.startsWith("if (p == null)") + ? "Accessing unknown field 'x' from 'null'" : "Might be null accessing 'x'"; + assertEquals(body,expected,e.getMessage()); + } + } + } + + @Test + public void testFuzzer() { + Parser parser = new Parser( +""" +int a = arg/3; +int b = arg*5; +int x = arg*7; +int y = arg/11; +int p; int g; int h; +if( (arg/13)==0 ) { + p = x + y; + g = x; + h = y; +} else { + p = a + b; + g = a; + h = b; +} +int r = g+h; +return p-r; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 0;", stop.toString()); + } + + @Test + public void testStruct() { + Parser parser = new Parser(""" +struct Bar { + int a; + int b; +} +struct Foo { + int x; +} +Foo? foo = null; +Bar bar = new Bar; +bar.a = 1; +bar.a = 2; +return bar.a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 2;", stop.toString()); + } + + @Test + public void testExample() { + Parser parser = new Parser(""" +struct Vector2D { int x; int y; } +Vector2D v = new Vector2D; +v.x = 1; +if (arg) + v.y = 2; +else + v.y = 3; +return v; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return new Vector2D;", stop.toString()); + } + + @Test + public void testBug() { + Parser parser = new Parser(""" +struct s0 { + int v0; +} +s0? v1=null; +int v3=v1.zAicm; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'zAicm' from 'null'",e.getMessage()); } + } + + @Test + public void testBug2() { + Parser parser = new Parser(""" +struct s0 { int v0; } +arg=0+new s0.0; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Expected an identifier, found 'null'",e.getMessage()); } + } + + @Test + public void testLoop() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar bar = new Bar; +while (arg) { + bar.a = bar.a + 2; + arg = arg + 1; +} +return bar.a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop10,0,(Phi_a+2));", stop.toString()); + } + + @Test + public void testIf() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar bar = new Bar; +if (arg) bar = null; +bar.a = 1; +return bar.a; +"""); + try { parser.parse().iterate(); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Bar",e.getMessage()); } + } + + @Test + public void testIf2() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar? bar = null; +if (arg) bar = new Bar; +bar.a = 1; +return bar.a; +"""); + try { parser.parse().iterate(); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'a'",e.getMessage()); } + } + + @Test + public void testIf3() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar bar = null; +if (arg) bar = null; +bar.a = 1; +return bar.a; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Bar", e.getMessage()); } + } + + @Test + public void testIfOrNull() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar? bar = new Bar; +if (arg) bar = null; +if( bar ) bar.a = 1; +return bar; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Region15,null,new Bar);", stop.toString()); + } + + @Test + public void testIfOrNull2() { + Parser parser = new Parser( +""" +struct Bar { int a; } +Bar? bar = new Bar; +if (arg) bar = null; +int rez = 3; +if( !bar ) rez=4; +else bar.a = 1; +return rez; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Region32,4,3);", stop.toString()); + } + + @Test + public void testWhileWithNullInside() { + Parser parser = new Parser(""" +struct s0 {int v0;} +s0? v0 = new s0; +int ret = 0; +while(arg) { + ret = v0.v0; + v0 = null; + arg = arg - 1; +} +return ret; +"""); + try { parser.parse().iterate(); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'v0'", e.getMessage()); } + } + + @Test + public void testRedeclareStruct() { + Parser parser = new Parser(""" +struct s0 { + int v0; +} +s0? v1=new s0; +s0? v1; +v1=new s0; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Redefining name 'v1'", e.getMessage()); } + } + + @Test + public void testIter() { + // Build and use an iterator + Parser parser = new Parser( +""" +struct Iter { + int x; + int len; +} +Iter i = new Iter; +i.len = arg; +int sum=0; +while( i.x < i.len ) { + sum = sum + i.x; + i.x = i.x + 1; +} +return sum; +"""); + StopNode stop = parser.parse().iterate(); + // The preceding len store keeps the initial x load on the bulk chain. + assertEquals("return Phi(Loop13,0,(Phi(Loop,.x,(Phi_x+1))+Phi_sum));", stop.toString()); + assertEquals(10L, com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,5)); + } + + + + + @Test + public void test1() { + Parser parser = new Parser(""" +struct s0 {int v0;} +s0 ret = new s0; +while(arg) { + s0 v0 = new s0; + v0.v0 = arg; + arg = arg-1; + if (arg==5) ret=v0; +} +return ret; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop10,new s0,Phi(Region31,new s0,Phi_ret));", stop.toString()); + } + + @Test + public void test2() { + Parser parser = new Parser(""" +struct s0 {int v0;} +s0 ret = new s0; +s0 v0 = new s0; +while(arg) { + v0.v0 = arg; + arg = arg-1; + if (arg==5) ret=v0; +} +return ret; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop13,new s0,Phi(Region32,new s0,Phi_ret));", stop.toString()); + } + + + @Test + public void test3() { + Parser parser = new Parser(""" +struct s0 {int v0;} +s0 ret = new s0; +while(arg < 10) { + s0 v0 = new s0; + if (arg == 5) ret=v0; + arg = arg + 1; +} +return ret; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop10,new s0,Phi(Region30,new s0,Phi_ret));", stop.toString()); + } + + @Test + public void testBug3() { + Parser parser = new Parser(""" +struct s0 { + int f0; +} +if(0>=0) return new s0; +return new s0; +int v0=null.f0; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'f0' from 'null'", e.getMessage()); } + } + + @Test + public void testBug4() { + Parser parser = new Parser(""" +if(0) { + while(0) if(arg) continue; + int v0=0; + while(1) { + int arg=-arg; + v0=arg; + } +} + """); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testBug5() { + Parser parser = new Parser(""" +struct s0 { + int f0; +} +if(0) return 0; +else return new s0; +if(new s0.f0) return 0; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return new s0;", stop.toString()); + } + + @Test + public void testBug6MissedWorklist() { + Parser parser = new Parser(""" +while(0) {} +int v4=0; +while(0<arg) { + v4=v4+1; + while(1) v4=-v4; + while(0) arg=-1; +} +return 0; + """); + StopNode stop = parser.parse().iterate(); + } + + @Test + public void testBug7() { + Parser parser = new Parser(""" +struct s0 { int f0; } +s0 v0 = new s0; +while(v0.f0) {} +s0 v1 = v0; +return v1; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return new s0;", stop.toString()); + } + + + @Test + public void testBug8() { + Parser parser = new Parser(""" +int v2=0; +while(0) +while(0) {} +{ + { + { + int v36=0; + { + while(0) { + { + while(-v2) { + { + while(v36) { + while(v2) return 0; + break; + } } + if(-v2) break; + } + } + } + } + } } +} +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testBug9() { + Parser parser = new Parser(""" +int v0=arg==0; +while(v0) continue; +return 0; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 0;", stop.toString()); + } + + + @Test public void testReadBeforeStores() { + var stop = new Parser(""" + struct S { int x; int y; } + S a = new S; S b = new S; + a.x = 11; b.x = 22; + S p = a; if (arg) p = b; + int before = p.x; + a.y = 55; b.y = 66; + a.x = 33; b.x = 44; + return before*100 + p.x; + """).parse().iterate(); + assertEquals(1133L, com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,0)); + assertEquals(2244L, com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,1)); + } + + @Test public void testMemoryAcrossNestedLoops() { + var stop = new Parser(""" + struct S { int x; int y; int z; } + S s = new S; + s.x = 5; s.y = 7; s.z = 11; + while (arg > 0) { + s.x = s.x + 1; + int j = 3; + while (j > 0) { + j = j - 1; + if (j == 1) continue; + s.y = s.y + arg; + if (arg == 2) break; + s.z = s.z + 1; + } + arg = arg - 1; + } + return s; + """).parse().iterate(); + for (int arg=0; arg<7; arg++) { + var obj = (com.seaofnodes.simple.evaluator.Evaluator.Obj) + com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,arg); + org.junit.Assert.assertArrayEquals(new Object[] { + 5L+arg, 7L+arg*(arg+1)-(arg>=2 ? 2 : 0), 11L+2*arg-(arg>=2 ? 2 : 0) + }, obj.fields()); + } + } + + @Test public void testMemoryAtEarlyReturns() { + var stop = new Parser(""" + struct S { int x; int y; int z; } + S s = new S; + s.x = 3; s.y = 5; s.z = 7; + if (arg) { s.x = 11; s.z = 13; return s; } + s.y = 17; + return s; + """).parse().iterate(); + for (int arg=0; arg<2; arg++) { + var obj = (com.seaofnodes.simple.evaluator.Evaluator.Obj) + com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,arg); + org.junit.Assert.assertArrayEquals(arg==0 ? new Object[]{3L,17L,7L} : new Object[]{11L,5L,13L}, obj.fields()); + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/SchedulerTest.java b/src/test/java/com/seaofnodes/simple/SchedulerTest.java new file mode 100644 index 00000000..ad595822 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/SchedulerTest.java @@ -0,0 +1,73 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.evaluator.Evaluator; +import com.seaofnodes.simple.node.StopNode; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SchedulerTest { + + private static void assertObj(Object obj, String name, Object... fields) { + assertEquals(Evaluator.Obj.class, obj.getClass()); + var o = (Evaluator.Obj)obj; + assertEquals(name, o.struct()._name); + Assert.assertArrayEquals(fields, o.fields()); + } + + @Test + @Ignore + public void testJig() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v=new S; +v.f = 2; +int i=v.f; +if (arg) v.f=1; +return i; +"""); + StopNode stop = parser.parse();//.iterate(); + var eval = new Evaluator(stop); + assertEquals(2L, eval.evaluate(1, 10)); + } + + @Test + public void testStoreInIf() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v0=new S; +if(arg) v0.f=1; +return v0; +"""); + StopNode stop = parser.parse().iterate(); + var eval = new Evaluator(stop); + assertObj(eval.evaluate(0, 10), "S", 0L); + assertObj(eval.evaluate(1, 10), "S", 1L); + } + + @Test + public void testStoreInIf2() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v=new S; +v.f = 2; +int i=new S.f; +i=v.f; +if (arg) v.f=1; +return i; +"""); + StopNode stop = parser.parse().iterate(); + var eval = new Evaluator(stop); + assertEquals(2L, eval.evaluate(0, 10)); + assertEquals(2L, eval.evaluate(1, 10)); + } + +} diff --git a/src/test/java/com/seaofnodes/simple/TypeTest.java b/src/test/java/com/seaofnodes/simple/TypeTest.java new file mode 100644 index 00000000..c9eb2747 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -0,0 +1,132 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.type.*; +import org.junit.Assert; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class TypeTest { + + // Test basic properties and GLB + @Test + public void testTypeAdHoc() { + TypeStruct s1 = TypeStruct.make("s1", new Field[]{ + Field.make("a", TypeInteger.BOT), + Field.make("b", TypeInteger.BOT) }); + TypeStruct s2 = TypeStruct.make("s2", new Field[]{ + Field.make("a", TypeInteger.BOT), + Field.make("b", TypeInteger.BOT) }); + Assert.assertEquals(s1, s1.glb()); + Assert.assertNotEquals(s1, s1.dual()); + Assert.assertEquals(s1, s1.dual().glb()); + + assertEquals(TypeStruct.BOT, s1.meet(s2)); + assertEquals(TypeMem.BOT, TypeMem.TOP.meet(TypeMem.BOT)); + assertEquals(TypeMem.TOP, TypeMem.BOT.dual()); + assertEquals(TypeMem.BOT, TypeMem.TOP.dual()); + + TypeMemPtr ptr1 = TypeMemPtr.make(s1); + Assert.assertEquals(s1, ptr1._obj); + TypeMemPtr ptr2 = TypeMemPtr.make(s2); + Assert.assertEquals(s2, ptr2._obj); + + TypeMemPtr ptr1nil = TypeMemPtr.make(s1, true); + Assert.assertEquals(s1, ptr1nil._obj); + Assert.assertTrue(ptr1nil._nil); + Assert.assertFalse(ptr1nil._obj==null); + TypeMemPtr ptr2nil = TypeMemPtr.make(s2, true); + Assert.assertEquals(s2, ptr2nil._obj); + + Assert.assertNotEquals(ptr1, ptr2); + Assert.assertNotEquals(ptr1, ptr1.glb()); + Assert.assertEquals(ptr1nil, ptr1.glb()); + + Assert.assertEquals(ptr1, ptr1.dual().dual()); + Assert.assertEquals(ptr1.glb(), ptr1.dual().glb()); + Assert.assertEquals(TypeMemPtr.BOT, ptr1.meet(ptr2nil)); + Assert.assertEquals(ptr1.glb(), ptr1.meet(TypeMemPtr.NULLPTR)); + + TypeMemPtr TOP = TypeMemPtr.TOP; + TypeMemPtr BOT = TypeMemPtr.BOT; + TypeMemPtr PTR = TypeMemPtr.VOIDPTR; + TypeMemPtr NULL = TypeMemPtr.NULLPTR; + Type PTR_meet_NULL = NULL.meet(PTR); + Assert.assertEquals(BOT, PTR_meet_NULL); + Type ptr1_meet_ptr2 = ptr1.meet(ptr2); + Assert.assertEquals(PTR, ptr1_meet_ptr2); + Type NULL_join_ptr1 = NULL.join(ptr1); + Assert.assertEquals(TOP, NULL_join_ptr1); + Type NULL_join_PTR = PTR.join(NULL); + Assert.assertEquals(TOP, NULL_join_PTR); + Type ptr1_dual = ptr1.dual(); + Type nullableptr1_dual = ptr1nil.dual(); + } + + // Test theoretical properties. + // This is a symmetric complete bounded (ranked) lattice. + // Also the meet is commutative and associative. + // The lattice has a dual (symmetric), and join is ~(~x meet ~y). + // See https://en.wikipedia.org/wiki/Lattice_(order). + @Test + public void testLatticeTheory() { + Type[] ts = Type.gather(); + + // Confirm commutative & complete + for( Type t0 : ts ) + for( Type t1 : ts ) { + check_commute (t0,t1); + check_symmetric(t0,t1); + } + + // Confirm associative + for( Type t0 : ts ) + for( Type t1 : ts ) + for( Type t2 : ts ) + assoc(t0,t1,t2); + + // Confirm symmetry. If A isa B, then A.join(C) isa B.join(C) + for( Type t0 : ts ) + for( Type t1 : ts ) + if( t0.isa(t1) ) + for( Type t2 : ts ) { + Type t02 = t0.join(t2); + Type t12 = t1.join(t2); + Type mt = t02.meet(t12); + assertSame(mt,t12); + } + } + + // By design in meet, args are already flipped to order _type, which forces + // symmetry for things with badly ordered _type fields. The question is + // still interesting for other orders. + private static void check_commute( Type t0, Type t1 ) { + if( t0==t1 ) return; + if( t0.is_simple() && !t1.is_simple() ) return; // By design, flipped the only allowed order + Type mta = t0.meet(t1); + Type mtb = t1.meet(t0); // Reverse args and try again + assertSame(mta,mtb); + } + + // A & B = MT + // Expect: ~A & ~MT == ~A + // Expect: ~B & ~MT == ~B + private static void check_symmetric( Type t0, Type t1 ) { + if( t1==t0 ) return; + Type mt = t0.meet(t1); + Type ta = mt.dual().meet(t1.dual()); + Type tb = mt.dual().meet(t0.dual()); + assertSame(ta,t1.dual()); + assertSame(tb,t0.dual()); + } + + private static void assoc( Type t0, Type t1, Type t2 ) { + Type t01 = t0 .meet(t1 ); + Type t12 = t1 .meet(t2 ); + Type t01_2 = t01.meet(t2 ); + Type t0_12 = t0 .meet(t12); + assertSame(t01_2,t0_12); + } + +} diff --git a/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java b/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java new file mode 100644 index 00000000..eaeb061b --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java @@ -0,0 +1,209 @@ +package com.seaofnodes.simple.evaluator; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Field; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.type.TypeStruct; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Objects; + +public class Evaluator { + + private static final Object MEMORY = new Object(); + + public record Obj(TypeStruct struct, Object[] fields) { + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("Obj<").append(struct._name).append("> {"); + for(int i=0; i<struct._fields.length; i++) { + sb.append("\n ").append(struct._fields[i]._fname).append("=").append(fields[i]); + } + return sb.append("\n}").toString(); + } + } + + private static int getFieldIndex(TypeStruct struct, MemOpNode memop) { + int idx = struct.find(memop._name); + if( idx >= 0 ) return idx; + throw new AssertionError("Field "+memop._name+" not found in struct " + struct._name); + } + + public enum Status { + TIMEOUT, + FALLTHROUGH + } + + /** + * Find the start node from some node in the graph or null if there is no start node + */ + private static StartNode findStart(BitSet visit, Node node) { + if (node == null) return null; + if( visit.get(node._nid) ) return null; + visit.set(node._nid); + StartNode start = node instanceof StartNode ? (StartNode) node : null; + for( Node def : node._inputs ) { + var res = findStart(visit,def); + if( res != null ) start = res; + } + for( Node use : node._outputs ) { + var res = findStart(visit,use); + if( res != null ) start = res; + } + return start; + } + + private final Object[] values; + private final ArrayList<Object> phiCache = new ArrayList<>(); + private final StartNode start; + private final Scheduler.Block startBlock; + + public Evaluator(Node graph) { + var visited = new BitSet(); + this.start = findStart(visited, graph); + if (this.start == null) { + this.values = null; + this.startBlock = null; + return; + } + this.startBlock = Scheduler.schedule(this.start); + this.values = new Object[visited.length()]; + } + + private long div(DivNode div) { + long in2 = vall(div.in(2)); + return in2 == 0 ? 0 : vall(div.in(1)) / in2; + } + + private Object alloc(NewNode alloc) { + var type = ((TypeMemPtr)alloc.compute())._obj; + return new Obj(type, new Object[type._fields.length]); + } + + private Object load(LoadNode load) { + var from = (Obj)val(load.in(2)); + var idx = getFieldIndex(from.struct, load); + return from.fields[idx]; + } + + private Object store(StoreNode store) { + var to = (Obj)val(store.in(2)); + var val = val(store.in(3)); + var idx = getFieldIndex(to.struct, store); + to.fields[idx] = val; + return null; + } + + private static boolean isTrue(Object obj) { + if (obj == null) return false; + if (obj instanceof Number n) return n.longValue() != 0; + return true; + } + + private Object val(Node node) { + return values[node._nid]; + } + + private long vall(Node node) { + var v = val(node); + if (v instanceof Number n) return n.longValue(); + throw new AssertionError("Not a long " + v); + } + + private Object cons(ConstantNode cons) { + var type = cons.compute(); + if (type instanceof TypeInteger i) return i.value(); + assert type instanceof TypeMemPtr; + return null; + } + + private Object exec(Node node) { + return switch (node) { + case ConstantNode cons -> cons(cons); + case AddNode add -> vall(add.in(1)) + vall(add.in(2)); + case BoolNode.EQ eq -> Objects.equals(val(eq.in(1)), val(eq.in(2))) ? 1L : 0L; + case BoolNode.LE le -> vall(le.in(1)) <= vall(le.in(2)) ? 1L : 0L; + case BoolNode.LT lt -> vall(lt.in(1)) < vall(lt.in(2)) ? 1L : 0L; + case DivNode div -> div(div); + case MinusNode minus -> -vall(minus.in(1)); + case MulNode mul -> vall(mul.in(1)) * vall(mul.in(2)); + case NotNode not -> isTrue(val(not.in(1))) ? 0L : 1L; + case SubNode sub -> vall(sub.in(1)) - vall(sub.in(2)); + case CastNode cast -> val(cast.in(1)); + case LoadNode load -> load(load); + case StoreNode store -> store(store); + case NewNode alloc -> alloc(alloc); + case ProjNode proj -> ((Object[])val(proj.ctrl()))[proj._idx]; + default -> throw new AssertionError("Unexpected node " + node); + }; + } + + /** + * Run the graph until either a return is found or the number of loop iterations are done. + */ + public Object evaluate(long parameter, int loops) { + if (start == null) return Status.TIMEOUT; + var s = new Object[start.compute()._types.length]; + s[1] = MEMORY; + s[2] = parameter; + values[start._nid] = s; + int i=0; + Scheduler.Block block = this.startBlock; + for(;;) { + for(; i<block.nodes().length;i++) values[block.nodes()[i]._nid] = exec(block.nodes()[i]); + i=0; + switch (block.exit()) { + case null: + return Status.FALLTHROUGH; + case ReturnNode ret: + return val(ret.in(1)); + case IfNode ifn: + block = block.next()[isTrue(val(ifn.in(1))) ? 0 : 1]; + if (block == null) return Status.FALLTHROUGH; + break; + case RegionNode region: + if (loops-- <= 0) return Status.TIMEOUT; + int exit = block.exitId(); + assert exit > 0 && region.nIns() > exit; + block = block.next()[0]; + assert block != null; + for (; i < block.nodes().length; i++) { + if (!(block.nodes()[i] instanceof PhiNode)) break; + phiCache.add(val(block.nodes()[i].in(exit))); + } + for (i=0; i<phiCache.size(); i++) { + values[block.nodes()[i]._nid] = phiCache.get(i); + } + phiCache.clear(); + break; + default: + throw Utils.TODO("Unexpected control node " + block.exit()); + } + } + } + + public static Object evaluate(StopNode graph) { + return evaluate(graph, 0); + } + + public static Object evaluate(StopNode graph, long parameter) { + return evaluate(graph, parameter, 1000); + } + + public static Object evaluate(StopNode graph, long parameter, int loops) { + var res = evaluateWithResult(graph, parameter, loops); + if (res == Status.TIMEOUT) throw new RuntimeException("Timeout"); + return res; + } + + public static Object evaluateWithResult(StopNode graph, long parameter, int loops) { + return new Evaluator(graph).evaluate(parameter, loops); + } + +} diff --git a/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java b/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java new file mode 100644 index 00000000..674d12f5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java @@ -0,0 +1,592 @@ +package com.seaofnodes.simple.evaluator; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; + +import java.util.*; + +/** + * A late scheduler for test cases. + * <br> + * <br> + * This scheduler is used for tests and is not the final scheduler. + * <br> + * <br> + * The scheduler schedules not as late as possible. Since it is just for test cases this is fine. + * A better scheduler will try to place nodes outside of loops. Due to the schedule late strategy + * this scheduler might place initialization code in loops. + * <br> + * We schedule late here since memory edges have some implicit information. There can only be one + * memory edge (for a field) at a time. However, before <code>if</code> statements this might split. + * When scheduling early a store might be moved out of the loop but then there are two edges alive. + * The one through the store node and the adjacent one. This is an invalid schedule and instead of + * handling this case a late scheduler does not have this problem as memory edges are combined + * through phi nodes. + * <br> + * <br> + * A bit about this scheduler. It first discovers all alive nodes and allocates side data for them. + * Then the control flow graph is build. Then, all phis are placed and finally all the remaining nodes + * in an order where a node is placed when all alive uses are placed. Finally, the output is produced. + */ +public class Scheduler { + + /** + * A basic block with a schedule of the containing nodes. + * + * @param nodes The nodes in the final schedule of this block + * @param exit The exit node of this block. + * @param exitId In case the next block is a region this gives the entry id into the region. + * @param next The next blocks. + */ + public record Block(Node[] nodes, Node exit, int exitId, Block[] next) {} + + /** + * Definition of a block in the building process. + */ + private static class BasicBlock { + + /** + * Dominator of this block + */ + final BasicBlock dom; + /** + * Id of this block where <code>a.depth < b.depth</code> means that a is before b or + * both are in different branches. + */ + final int depth; + /** + * Previous blocks + */ + final BasicBlock[] prev; + /** + * Entry to this block + */ + final Node entry; + /** + * Schedules node in reverse order. + */ + final ArrayList<Node> reverseSchedule = new ArrayList<>(); + + /** + * Initialize the block. + * @param entry The node that started this block + * @param depth The depth of this node. All previous nodes need to have a lower value. + * @param prev The previous blocks. + */ + BasicBlock(Node entry, int depth, BasicBlock... prev) { + BasicBlock dom = null; + int max = -1; + for(var bb: prev) { + max = Math.max(max, bb.depth); + dom = dom==null?bb:dom(dom, bb); + } + assert depth > max; + this.dom = dom; + this.depth = depth; + this.entry = entry; + this.prev = prev; + } + + } + + /** + * Ancillary data for nodes used during the scheduling process. + */ + private static class NodeData { + + /** + * The node this ancillary data is used for. + */ + final Node node; + + /** + * Number of alive users not yet scheduled. + */ + int users = 1; + + /** + * Block into which the node should be placed so far. + * This will be updated with every placed use. + */ + BasicBlock block; + + /** + * Initialize ancillary data for a node + * @param node The node this ancillary data is associated with. + */ + NodeData(Node node) { + this.node = node; + } + + } + + /** + * Ancillary data for nodes. + */ + private final IdentityHashMap<Node, NodeData> data = new IdentityHashMap<>(); + /** + * List of nodes which can be placed since all users are placed. + */ + private final Stack<NodeData> scheduleQueue = new Stack<>(); + + /** + * Get the ancillary data for an alive node. + * @param node The node for which the ancillary data is requested. + * @return The ancillary data for the node. + */ + NodeData d(Node node) { + return od(node).orElseThrow(AssertionError::new); + } + + /** + * Get the ancillary data for a potentially dead node. + * @param node The node for which the ancillary data is requested. + * @return The ancillary data for the node an alive node or + * <code>Optional.empty()</code> for <code>null</code> or dead nodes. + */ + Optional<NodeData> od(Node node) { + return Optional.ofNullable(data.get(node)); + } + + /** + * Checks a node for validity + * @param data The node to check. + * @return true if all placed inputs are before this node. + */ + private boolean isValid(NodeData data) { + return data.node._inputs.stream().map(i->i==null?null:d(i)).map(d->d==null||d.users>0?null:d.block).allMatch(d->d==null||dom(d, data.block)==d); + } + + /** + * Checks that node is not XCtrl. + * @param node The node to check. + * @return true if the node is not XCtrl. + */ + private static boolean isNotXCtrl(Node node) { + return !(node instanceof ConstantNode c && c.compute() == Type.XCONTROL); + } + + /** + * Return the dominator of <code>a</code> and <code>b</code> + * @param a Block a + * @param b Block b + * @return The dominator of <code>a</code> and <code>b</code> + */ + private static BasicBlock dom(BasicBlock a, BasicBlock b) { + while (a!=b) { + if (a.depth >= b.depth) a=a.dom; + if (b.depth > a.depth) b=b.dom; + } + return a; + } + + /** + * Return if a is before b. + * @param a Block a + * @param b Block b + * @return true is a is before b. + */ + private static boolean isBefore(BasicBlock a, BasicBlock b) { + while (b.depth > a.depth) { + while (b.dom.depth > a.depth) b = b.dom; + for (int i=0; i<b.prev.length-1; i++) { + if (isBefore(a, b.prev[i])) return true; + } + b = b.prev[b.prev.length-1]; + } + return a == b; + } + + /** + * Is node placed during the control flow graph build. + * @param data Node to check. + * @return true if node is placed during the control flow graph build. + */ + private static boolean isPinnedNode(NodeData data) { + return data.node.isCFG() || data.node instanceof PhiNode; + } + + /** + * Refine placement of a node. + * @param data The node to refine. + * @param block The block before the node should be scheduled. + */ + private void refinePlacement(NodeData data, BasicBlock block) { + assert !isPinnedNode(data); + data.block = data.block == null ? block : dom(data.block, block); + assert isValid(data); + } + + /** + * Refine placement if before is happening before data + * @param data The node for which the placement should be refined. + * @param before The node before which the data node should be placed. + */ + private void optionalRefinePlacement(NodeData data, Node before) { + od(before).ifPresent(d->{ + // before might be in a different branch. So check this case with isBefore + // and only refine the placement if it is not in a different branch. + if (isBefore(d.block, data.block)) refinePlacement(data, d.block); + }); + } + + /** + * Schedule all nodes not yet scheduled. + */ + private void doSchedule() { + while (!scheduleQueue.isEmpty()) { + var data = scheduleQueue.pop(); + assert !isPinnedNode(data); + + assert data.block != null; + assert data.users == 0; + + if (data.node instanceof LoadNode l) { + assert isValid(data); + // Handle anti-deps of load nodes. + // At this point all anti-dep nodes are scheduled, + // but they did not refine the placement + // so do that now. + var mem = l.in(1); + for(var out : mem._outputs) { + if (out instanceof PhiNode p) { + var r = p.in(0); + for (int i = 1; i < p.nIns(); i++) { + if (p.in(i) == mem) optionalRefinePlacement(data, r.in(i)); + } + } else if (!(out instanceof LoadNode)) { + optionalRefinePlacement(data, out); + } + } + } + + assert isValid(data); + data.block.reverseSchedule.add(data.node); + + for(var in:data.node._inputs) { + if (in!=null) update(d(in), data.block); + } + if (data.node instanceof StoreNode s) { + // Store nodes have anti-deps to load nodes. + // So decrease the uses of these loads when the store is placed. + for (var out: s.in(1)._outputs) { + if (out instanceof LoadNode) od(out).ifPresent(this::decUsers); + } + } + } + + // Now all nodes should be placed and have a block assigned + assert this.data.values().stream().noneMatch(d->d.block==null); + } + + /** + * Decrement the not-yet-placed users of a block. + * @param data The node for which the users should be decremented. + */ + private void decUsers(NodeData data) { + assert data.users > 0; + if (--data.users == 0) { + assert data.block != null; + assert isValid(data); + + // When all users are gone this node can be scheduled. + scheduleQueue.push(data); + } + } + + /** + * Update the placement of a node + * @param data The node to update + * @param block The block before which the node should happen. + */ + private void update(NodeData data, BasicBlock block) { + assert block != null; + if (data.users == 0) { + assert isPinnedNode(data); + return; + } + refinePlacement(data, block); + decUsers(data); + } + + /** + * Checks if a CFG node is ready to be placed. This is used for regions joining branches. + * @param node The CFG node to check + * @return true if all parents of a CFG node are placed. + */ + private boolean isCFGNodeReady(Node node) { + if (node instanceof LoopNode l) { + assert d(l.in(1)).block != null; + } else if (node instanceof RegionNode r) { + for(int i=1; i<r.nIns(); i++) { + if (od(r.in(i)).map(d->d.block==null).orElse(false)) return false; + } + } else { + assert d(node.in(0)).block != null; + } + return true; + } + + /** + * Schedule all phi nodes. + * @param phiQueue List of all the phi nodes to schedule. + */ + private void schedulePhis(Stack<NodeData> phiQueue) { + while(!phiQueue.empty()) { + var data = phiQueue.pop(); + var phi = (PhiNode) data.node; + var r = phi.in(0); + data.block = d(r).block; + for(int i=1; i<phi.nIns(); i++) { + var in = phi.in(i); + od(r.in(i)).ifPresent(d->update(d(in), d.block)); + } + } + } + + /** + * Build the control flow graph. + * @param start The start node. + */ + private void doBuildCTF(StartNode start) { + var queue = new Stack<NodeData>(); + var phiQueue = new Stack<NodeData>(); + queue.push(d(start)); + int depth = 0; + while (!queue.isEmpty()) { + var data = queue.pop(); + var node = data.node; + assert node.isCFG(); + BasicBlock block; + switch (node) { + case StartNode s: + block = new BasicBlock(s, depth++); + break; + case LoopNode l: + block = new BasicBlock(l, depth++, d(l.in(1)).block); + break; + case RegionNode r: + var prev = new ArrayList<BasicBlock>(); + for(int i=1; i<r.nIns(); i++) { + od(r.in(i)).ifPresent(d->prev.add(d.block)); + } + block = new BasicBlock(r, depth++, prev.toArray(BasicBlock[]::new)); + break; + case IfNode i: + block = d(i.in(0)).block; + break; + case ReturnNode r: + block = d(r.in(0)).block; + break; + default: + block = new BasicBlock(node, depth++, d(node.in(0)).block); + } + data.block = block; + if (node instanceof RegionNode r) { + // Regions might have phis which need to be scheduled. + // Put them on a list for later scheduling. + for (var out:r._outputs) { + if (out instanceof PhiNode p) { + var d = this.data.get(p); + if (d!=null) { + d.users=0; + phiQueue.push(d); + } + } + } + } + if (!(node instanceof ReturnNode)) + for (Node n:node._outputs) if (n.isCFG() && isCFGNodeReady(n) && d(n).block == null) queue.push(d(n)); + var b = block; + for(var in:data.node._inputs) od(in).ifPresent(d->update(d, b)); + } + + // Schedule all phi nodes. + schedulePhis(phiQueue); + } + + /** + * Mark a node alive and create ancillary data for it. + * @param queue List into which this node should be placed if it needs to be visited. + * @param node Node which should be marked alive. + * @param cfg If this node is a CFG node. + */ + private void markAlive(Stack<NodeData> queue, Node node, boolean cfg) { + var nd = od(node).orElse(null); + if (nd != null) { + // Node was already visited, just increase the users. + if (nd.users>0) nd.users++; + return; + } + assert node.isCFG() == cfg; + assert isNotXCtrl(node); + nd = new NodeData(node); + if (cfg) nd.users=0; + data.put(node, nd); + queue.push(nd); + } + + /** + * Mark all alive nodes. + * First all CFG nodes are marked, then all other nodes. + * @param node THe start node. + */ + private void doMarkAlive(Node node) { + var cfgQueue = new Stack<NodeData>(); + var dataQueue = new Stack<NodeData>(); + var mem = new Stack<NodeData>(); + markAlive(cfgQueue, node, true); + // Mark all CFG nodes. + while (!cfgQueue.isEmpty()) { + var data = cfgQueue.pop(); + node = data.node; + assert node.isCFG(); + if (!(node instanceof ReturnNode)) { + for (var out : node._outputs) if (out.isCFG() && isNotXCtrl(out)) markAlive(cfgQueue, out, true); + } + for (var in : node._inputs) if(in!=null && !in.isCFG() && isNotXCtrl(in)) markAlive(dataQueue, in, false); + } + // Mark all other nodes. + while (!dataQueue.isEmpty()) { + var data = dataQueue.pop(); + node = data.node; + assert !node.isCFG(); + if (node instanceof PhiNode phi) { + var r = phi.in(0); + for (int i=1; i<phi.nIns(); i++) { + if (od(r.in(i)).isPresent()) markAlive(dataQueue, phi.in(i), false); + } + } else { + for (var in : node._inputs) if (in != null && !in.isCFG()) markAlive(dataQueue, in, false); + } + if (node instanceof StoreNode) mem.push(data); + } + // Handle store nodes and increase load with an anti-dep to the store. + while (!mem.isEmpty()) { + var data = mem.pop(); + node = data.node; + for(var out:node.in(1)._outputs) { + if (out instanceof LoadNode) od(out).ifPresent(d->d.users++); + } + } + } + + /** + * Helper function to append the nodes of a block to an array of nodes + * @param arr The array the nodes of a block should be appended to. + * @param node A potentially separator node + * @param block The block from which the nodes should be appended. + * @return The combined new array. + */ + private static Node[] appendNodes(Node[] arr, Node node, BasicBlock block) { + var idx = arr.length; + arr = Arrays.copyOf(arr, arr.length+block.reverseSchedule.size()+(node==null?0:1)); + if (node != null) arr[idx++] = node; + for(int i=block.reverseSchedule.size()-1;i>=0;i--) { + arr[idx++] = block.reverseSchedule.get(i); + } + return arr; + } + + /** + * Find the CFG output of a node + * @param node The node + * @return THe CFG output of the node. + */ + private static Node findSingleCFGOut(Node node) { + if (node instanceof StartNode) { + for(var n:node._outputs) if (n instanceof ProjNode p && p._idx==0) return p; + return null; + } + assert node._outputs.stream().filter(Node::isCFG).limit(2).count()<=1; + for(var n:node._outputs) if(n.isCFG()) return n; + return null; + } + + /** + * Build the final data structure after all the scheduling happened. + * @param start The start node + * @return The final schedule. + */ + private Block build(Node start) { + var blocks = new IdentityHashMap<Node, Block>(); + var queue = new Stack<NodeData>(); + queue.push(d(start)); + + // Visit all CFG nodes and create blocks for them. + // This can combine blocks. + while(!queue.isEmpty()) { + var data = queue.pop(); + var first = data.node; + Node prev; + var last = first; + Node[] arr = new Node[0]; + if (first instanceof RegionNode) { + var nodes = new ArrayList<Node>(); + for(var out: first._outputs) { + if (out instanceof PhiNode) { + if(od(out).isPresent()) nodes.add(out); + } + } + arr = nodes.toArray(arr); + } + arr = appendNodes(arr, null, data.block); + assert !(first instanceof IfNode); + for(;;) { + prev = last; + last = findSingleCFGOut(last); + if (last == null) break; + if (last instanceof RegionNode) { + if (blocks.get(last) == null && last != first) + queue.push(d(last)); + break; + } + if (last instanceof ReturnNode) break; + if (last instanceof IfNode if_) { + for(var out:if_._outputs) if (out.isCFG() && blocks.get(out) == null) queue.push(d(out)); + break; + } + data = d(last); + arr = appendNodes(arr, data.node, data.block); + } + blocks.put(first, switch(last) { + case null -> new Block(arr, null, -1, new Block[0]); + case IfNode i -> new Block(arr, i, -1, new Block[2]); + case RegionNode r -> new Block(arr, r, Utils.find(r._inputs, prev), new Block[1]); + case ReturnNode r -> new Block(arr, r, -1, new Block[0]); + default -> throw new AssertionError("Unexpected block exit node"); + }); + } + // Update the next pointer of all blocks. + for (var block: blocks.values()) { + switch(block.exit) { + case null: break; + case IfNode i: + for(var out:i._outputs) if (out instanceof ProjNode p) block.next[p._idx] = blocks.get(p); + break; + case RegionNode r: + block.next[0] = blocks.get(r); + break; + default: + assert block.exit instanceof ReturnNode; + } + } + // And return the block for the start node. + return blocks.get(start); + } + + /** + * Create a schedule for the program reachable from start. + * @param start The start node + * @return The final schedule. + */ + public static Scheduler.Block schedule(StartNode start) { + var scheduler = new Scheduler(); + scheduler.doMarkAlive(start); + scheduler.doBuildCTF(start); + scheduler.doSchedule(); + return scheduler.build(start); + } + +} diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java index c3444093..7dcb88e5 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -1,11 +1,12 @@ package com.seaofnodes.simple.fuzzer; -import com.seaofnodes.simple.GraphEvaluator; import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.evaluator.Evaluator; import com.seaofnodes.simple.node.Node; import com.seaofnodes.simple.node.StopNode; import java.util.ArrayList; +import java.util.Objects; import java.util.Random; import java.util.function.Consumer; @@ -36,13 +37,15 @@ public class Fuzzer { * @param script The script causing the exception * @param reproducer Reproducer which should be used to reproduce the test case. */ - private void recordException(Throwable e, String script, Consumer<String> reproducer) { + private void recordException(Throwable e, String script, Consumer<String> reproducer, long seed) { for (var ex : exceptions) { if (FuzzerUtils.isExceptionFromSameCause(ex, e)) return; } exceptions.add(e); System.out.println("========== Stack =========="); e.printStackTrace(System.out); + System.out.println("========== Seed =========="); + System.out.println(seed); System.out.println("========== Code ==========="); System.out.println(script); System.out.println("========= Reduced ========="); @@ -51,20 +54,30 @@ public class Fuzzer { System.out.flush(); } + private static boolean neq(Object a, Object b) { + if (Objects.equals(a, b)) return false; + if (a instanceof Evaluator.Obj ea && b instanceof Evaluator.Obj eb) { + if (ea.struct() != eb.struct()) return true; + for(int i=0;i<ea.fields().length;i++) { + if (neq(ea.fields()[i], eb.fields()[i])) return true; + } + return false; + } + return true; + } + /** * Check that two graphs result in the same output when supplied with a value - * @param stop1 Stop node of the first graph - * @param stop2 Stop node of the second graph + * @param e1 Evaluator for the first graph + * @param e2 Evaluator for the second graph * @param in The input value to both graphs to test for an equal output */ - private static void checkGraphs(StopNode stop1, StopNode stop2, long in) { - var e1 = GraphEvaluator.evaluateWithResult(stop1, in, EVAL_TIMEOUT); - var e2 = GraphEvaluator.evaluateWithResult(stop2, in, EVAL_TIMEOUT); - if (e1.type() == GraphEvaluator.ResultType.TIMEOUT || e2.type() == GraphEvaluator.ResultType.TIMEOUT) return; - if (e1.type() != e2.type()) - throw new RuntimeException("Different calculations types " + e1.type() + " vs " + e2.type()); - if (e1.value() != e2.value()) - throw new RuntimeException("Different calculations values " + e1.value() + " vs " + e2.value()); + private static void checkGraphs(Evaluator e1, Evaluator e2, long in) { + var r1 = e1.evaluate(in, EVAL_TIMEOUT); + var r2 = e2.evaluate(in, EVAL_TIMEOUT); + if (r1 == Evaluator.Status.TIMEOUT || r2 == Evaluator.Status.TIMEOUT) return; + if (neq(r1, r2)) + throw new RuntimeException("Different calculations values " + r1 + " vs " + r2); } /** @@ -91,9 +104,11 @@ public class Fuzzer { throw e1; } var stop2 = FuzzerUtils.parse(script, 456); - checkGraphs(stop1, stop2, 0); - checkGraphs(stop1, stop2, 1); - checkGraphs(stop1, stop2, 10); + var e1 = new Evaluator(stop1); + var e2 = new Evaluator(stop2); + checkGraphs(e1, e2, 0); + checkGraphs(e1, e2, 1); + checkGraphs(e1, e2, 10); } /** @@ -101,11 +116,11 @@ public class Fuzzer { * @param script The script to test. * @param valid If the script is valid or if it might contain syntax errors. */ - private void check(String script, boolean valid) { + private void check(String script, boolean valid, long seed) { try { runCheck(script, valid); } catch (Throwable e) { - recordException(e, script, s->runCheck(s, valid)); + recordException(e, script, s->runCheck(s, valid), seed); } } @@ -155,8 +170,8 @@ public class Fuzzer { public void fuzzPeeps(long seed) { var rand = new Random(seed); var sb = new StringBuilder(); - var valid = new ScriptGenerator(rand, sb, false).genProgram(); - check(sb.toString(), valid); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); + check(sb.toString(), valid, seed); } @@ -172,7 +187,7 @@ public class Fuzzer { public void fuzzPeepsRegression(long seed) { var rand = new Random(seed); var sb = new StringBuilder(); - var valid = new ScriptGenerator(rand, sb, false).genProgram(); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); try { runCheck(sb.toString(), valid); } catch (Throwable e) { diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java index 6488297b..f947a234 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -18,7 +18,7 @@ import java.util.BitSet; class FuzzerUtils { /** - * Get the value of a private field + * Get the val of a private field */ @SuppressWarnings("unchecked") public static <T> T getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { @@ -76,10 +76,12 @@ class FuzzerUtils { } /** - * Check that eiter one of the objects is null or that both are equal. + * Check that the messages are similar. */ - private static boolean equalOrNull(Object a, Object b) { + private static boolean similarMessages(String a, String b) { if (a == null || b == null) return true; + a = a.replaceAll("'\\w+'", "'normalized'").replaceAll("variable \\w+", "variable normalized"); + b = b.replaceAll("'\\w+'", "'normalized'").replaceAll("variable \\w+", "variable normalized"); return a.equals(b); } @@ -98,7 +100,7 @@ class FuzzerUtils { public static boolean isExceptionFromSameCause(Throwable a, Throwable b) { if (a == null || b == null) return a == b; // Handle the null case if (a.getClass() != b.getClass()) return false; // Different classes - if (!equalOrNull(a.getMessage(), b.getMessage())) return false; // Different messages. Null is allowed as wildcard + if (!similarMessages(a.getMessage(), b.getMessage())) return false; // Different messages. Null is allowed as wildcard var s1 = a.getStackTrace(); var s2 = b.getStackTrace(); if (!sameStackTraceOrigin(s1, s2)) return false; // Different stack trace origins diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java b/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java index 2ef0a3fd..3731b40c 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java @@ -1,7 +1,10 @@ package com.seaofnodes.simple.fuzzer; +import java.util.Arrays; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -13,35 +16,45 @@ import java.util.regex.Pattern; */ public class Reducer { + private interface Replacer extends BiConsumer<StringBuilder, Matcher> {} + /** * Holder for the rewrite rules */ - private record Replace(Pattern p, String r) {} + private record Replace(Pattern pattern, boolean iterate, Replacer... replacements) { + Replace(Pattern p, Object... replacements) { + this(p, replacements[0] == Boolean.TRUE, Arrays.stream(replacements).filter(o->!(o instanceof Boolean)).map(Replace::from).toArray(Replacer[]::new)); + } + private static Replacer from(Object o) { + if (o instanceof Replacer r) return r; + if (o instanceof Integer i) return (s, m)->s.append(m.group(i)); + if (o instanceof String r) return (s, m)->s.append(r); + throw new IllegalArgumentException(); + } + } /** * List of rewrite rules. */ private static final Replace[] REPLACEMENTS = { + new Replace(Pattern.compile("\\b(?:[^\\W0]|\\w\\w+)\\b"), "0", "1"), + new Replace(Pattern.compile("(?<=\\n|^)[^\\n]+(?=\\n|$)"), true, ""), new Replace(Pattern.compile("(?<=[+\\-*/<>=(])--"), ""), new Replace(Pattern.compile("\\b--"), "+"), - new Replace(Pattern.compile("\\b\\+-"), "-"), - new Replace(Pattern.compile("\\bfalse\\b"), "0"), - new Replace(Pattern.compile("\\btrue\\b"), "1"), new Replace(Pattern.compile("-"), ""), + new Replace(Pattern.compile("\\b\\+-"), "-"), new Replace(Pattern.compile("(?<![+\\-*/<>=])(?:-|\\*|/|>=|<=|>|<|!=|==)(?![+\\-*/<>=])"), "+"), - new Replace(Pattern.compile("\\{(?:[^{}]|\\{})+}"), "{}"), - new Replace(Pattern.compile("\\{((?:[^{}]|\\{})+)}"), "$1"), - new Replace(Pattern.compile("\\(([^()]+)\\)"), "$1"), - new Replace(Pattern.compile("\\b\\w+\\b"), "0"), - new Replace(Pattern.compile("\\b(?:[^\\W0]|\\w\\w+)\\b"), "1"), + new Replace(Pattern.compile("\\{((?:[^{}]|\\{})+)(?:\\n[ \\t]*)?}"), "", "{}", 1), + new Replace(Pattern.compile("(?<=\\n|^)([ \\t]*)\\{\\n(.*?)\\n\\1}", Pattern.DOTALL), 2), + new Replace(Pattern.compile("\\(([^()]+)\\)"), 1), new Replace(Pattern.compile("\\d+\\+"), ""), new Replace(Pattern.compile("\\+\\d+"), ""), - new Replace(Pattern.compile("(?<=\\n|^)[^\\n]*(?=\\n|$)"), "\n"), - new Replace(Pattern.compile("\\n+"), "\n"), + new Replace(Pattern.compile("\\n\\n+"), "\n"), new Replace(Pattern.compile("^\\n+|\\n+$"), ""), new Replace(Pattern.compile("else *"), ""), new Replace(Pattern.compile("\\bif\\([^()]+\\) *"), ""), new Replace(Pattern.compile("\\bwhile\\([^()]+\\) *"), ""), + new Replace(Pattern.compile("\\bstruct \\w+ \\{[^{}]*}"), "") }; /** @@ -60,24 +73,31 @@ public class Reducer { /** * Rewrites the script with one rewrite rule. */ - private String doReplacement(String script, Pattern pattern, String with) { + private String doReplacement(String script, Pattern pattern, Replacer[] replacements) { var matcher = pattern.matcher(script); var sb = new StringBuilder(); - int last = 0; - while (matcher.find()) { + var last = 0; + var skip = 0; + outer: + while (matcher.find(last + skip)) { + sb.append(script, last, matcher.start()); + last = matcher.end(); + skip = last == matcher.start() ? 1 : 0; var pos = sb.length(); - matcher.appendReplacement(sb, with); - var tail = sb.length(); - matcher.appendTail(sb); - if (tester.test(sb.toString())) { - sb.setLength(tail); - } else { + for (var replacement : replacements) { + replacement.accept(sb, matcher); + var tail = sb.length(); + sb.append(script, last, script.length()); + if (tester.test(sb.toString())) { + sb.setLength(tail); + continue outer; + } sb.setLength(pos); - sb.append(script, last, matcher.end()); } - last = matcher.end(); + last = matcher.start(); + skip = 1; } - matcher.appendTail(sb); + sb.append(script, last, script.length()); return sb.toString(); } @@ -85,13 +105,21 @@ public class Reducer { * Clean up variable names from the random ones generated by the fuzzer. */ private String cleanVariables(String script) { - var matcher = Pattern.compile("\\bint ([0-9a-zA-Z_]+)").matcher(script); - int num = 0; + var matcher = Pattern.compile("\\b([0-9a-zA-Z_]+) ([0-9a-zA-Z_]+)").matcher(script); + int[] num = {0, 0}; while (matcher.find()) { - var var = matcher.group(1); - if (var.matches("v\\d+")) continue; - while (script.contains("v"+num)) num++; - var n = script.replaceAll("\\b"+var+"\\b", "v" + num); + var type = matcher.group(1); + var var = matcher.group(2); + if (type.equals("new")) continue; + String prefix = "v"; + int t = 0; + if (type.equals("struct")) { + t = 1; + prefix = "s"; + } + if (var.matches(prefix+"\\d+")) continue; + while (script.contains(prefix+num[t])) num[t]++; + var n = script.replaceAll("\\b"+var+"\\b", prefix + num[t]); if (tester.test(n)) { script = n; } @@ -104,7 +132,11 @@ public class Reducer { */ private String doAllReplacements(String script) { for (var replace: REPLACEMENTS) { - script = doReplacement(script, replace.p, replace.r); + String old; + do { + old = script; + script = doReplacement(script, replace.pattern, replace.replacements); + } while (replace.iterate && !old.equals(script)); } return script; } diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java index ad98f7e2..155063ab 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -6,6 +6,7 @@ import com.seaofnodes.simple.node.ScopeNode; import java.util.ArrayList; import java.util.HashSet; import java.util.Random; +import java.util.function.Predicate; /** * Generate a pseudo random script. @@ -15,6 +16,13 @@ import java.util.Random; */ public class ScriptGenerator { + private static final int MAX_STATEMENTS_PER_BLOCK = 5; + private static final int MAX_BINARY_EXPRESSIONS_PER_EXPRESSION = 10; + private static final int MAX_UNARY_EXPRESSIONS_PER_EXPRESSION = 6; + private static final int MAX_NAME_LENGTH = 10; + private static final int MAX_BLOCK_DEPTH = 4; + private static final int MAX_EXPRESSION_DEPTH = 10; + /** * Number of spaces per indentation */ @@ -53,6 +61,51 @@ public class ScriptGenerator { */ private static final String[] UNARY_OP = {"-"}; + private static class Type { + final String name; + Type(String name) {this.name=name;} + boolean isa(Type other) { return this == other; } + } + + private static class TypeStruct extends Type { + record Field (String name, Type type) {} + final Field[] fields; + final TypeNullable nullable = new TypeNullable(this); + TypeStruct(String name, Field[] fields) { + super(name); + this.fields = fields; + } + boolean isa(Type other) { return this == other || other == nullable; } + } + + private static class TypeNullable extends Type { + + final Type base; + + TypeNullable(Type base) { + super(base.name+"?"); + this.base = base; + } + + } + + private static final Type TYPE_INT = new Type("int"); + + + private static class Variable { + final String name; + final Type declared; + final Variable shadowing; + Type type; + boolean shadowed = false; + + Variable(String name, Type type, Variable var) { + this.name = name; + this.declared = type; + this.type = type; + this.shadowing = var; + } + } /** * The random number generator used for random decisions. @@ -77,24 +130,33 @@ public class ScriptGenerator { /** * The depth of blocks allowed. */ - private int depth = 20; + private int depth = MAX_BLOCK_DEPTH; /** * The depth of expressions allowed. */ - private int exprDepth = 30; + private int exprDepth = MAX_EXPRESSION_DEPTH; /** * Current variables in scope. */ - private final ArrayList<String> variables = new ArrayList<>(); + private final ArrayList<Variable> variables = new ArrayList<>(); + /** + * All defined structs. + */ + private final ArrayList<TypeStruct> structs = new ArrayList<>(); + /** + * Allow to declare structs. + */ + private boolean allowStructs = true; /** * If this needs to generate valid scripts or is also allowed to generate invalid ones. */ - private final boolean generateValid; + private boolean generateValid; /** * If this script might be invalid. */ private boolean maybeInvalid = false; + /** * * @param random the random number generator. @@ -135,7 +197,7 @@ public class ScriptGenerator { * @return The random name generated */ private StringBuilder getRandomName() { - int len = random.nextInt(30) + 1; + int len = random.nextInt(MAX_NAME_LENGTH) + 1; StringBuilder sb = new StringBuilder(len); sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()-10))); for (int i=1; i<len; i++) @@ -144,6 +206,90 @@ public class ScriptGenerator { } /** + * Should invalid code be generated? + * @return True if invalid code should be generated + */ + private boolean generateInvalid() { + if (generateValid || random.nextInt(1000)>2) return false; + maybeInvalid = true; + return true; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return The index of the variable in variables or -1 + */ + private int lookup(String name) { + for (int i=variables.size()-1; i>=0; i--) { + if (variables.get(i).name.equals(name)) return i; + } + return -1; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return THe variable of null if not found + */ + private Variable lookupVar(String name) { + int idx = lookup(name); + return idx == -1 ? null : variables.get(idx); + } + + /** + * Add a variable to the current scope + * @param name The name of the variable + * @param type The declared type of the variable + */ + private void addVariable(String name, Type type) { + var old = lookupVar(name); + variables.add(new Variable(name, type, old)); + if (old != null) old.shadowed = true; + } + + /** + * Pop variables to level to + * @param to Level until which variables should be popped + */ + private void popVariables(int to) { + while (variables.size() > to) { + var v = variables.removeLast(); + if (v.shadowing != null) v.shadowing.shadowed = false; + } + } + + /** + * Get a random visible variable patching pred + * @param pred The predicate the variable needs to match + * @return A random visible variable matching pred or null if no variable mached pred. + */ + private Variable findVisibleVariablesMatching(Predicate<Variable> pred) { + int num=0; + for (var v:variables) { + if (!v.shadowed && pred.test(v)) num++; + } + if (num == 0) return null; + num = random.nextInt(num); + for (var v:variables) { + if (!v.shadowed && pred.test(v) && num-- == 0) return v; + } + throw new AssertionError(); + } + + /** + * Get a structure by name + * @param name Name of the structure + * @return The structure of null + */ + private TypeStruct getStruct(String name) { + for (var s:structs) { + if (s.name.equals(name)) return s; + } + return null; + } + + /** * Generate a variable name. This might be new random name or a name from an outer scope. * If the program is allowed to be invalid this can return keywords and already used variable names in the current scope. * @return The variable name. @@ -152,35 +298,67 @@ public class ScriptGenerator { if (currScopeStart > 0 && random.nextInt(10) > 7) { // Use a variable outside the current scope. var v = variables.get(random.nextInt(currScopeStart)); - if (variables.lastIndexOf(v) < currScopeStart) return v; + if (lookup(v.name) < currScopeStart) return v.name; if (!generateValid) { maybeInvalid = true; - return v; + return v.name; } } // Generate a new random variable name StringBuilder sb = getRandomName(); var v = sb.toString(); - if (!generateValid && (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart)) { + if (!generateValid && (KEYWORDS.contains(v) || lookup(v) >= currScopeStart)) { maybeInvalid = true; return v; } - while (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart) { + while (KEYWORDS.contains(v) || lookup(v) >= currScopeStart) { sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); v = sb.toString(); } return v; } + /** + * Get a random structure name + * @return A random structure name + */ + private String getStructName() { + if (!structs.isEmpty() && generateInvalid()) return structs.get(random.nextInt(structs.size())).name; + // Generate a new random struct name + StringBuilder sb = getRandomName(); + var v = sb.toString(); + if (!generateValid && (KEYWORDS.contains(v) || getStruct(v) != null)) { + maybeInvalid = true; + return v; + } + while (KEYWORDS.contains(v) || getStruct(v) != null) { + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); + v = sb.toString(); + } + return v; + } + /** + * Get a random type + * @return A random type + */ + private Type getType() { + if (structs.isEmpty() || random.nextBoolean()) return TYPE_INT; + var idx = random.nextInt(structs.size()); + TypeStruct struct = structs.get(idx); + if (random.nextBoolean()) return struct.nullable; + return struct; + } /** * Generates a program and writes it into the string builder supplied in the constructor. * @return If the program is allowed to be invalid returns if something potentially invalid was generated. */ public boolean genProgram() { + var oldGenerateValid = generateValid; + if (!generateValid && random.nextBoolean()) generateValid = true; this.maybeInvalid = false; - variables.add(ScopeNode.ARG0); + addVariable(ScopeNode.ARG0, TYPE_INT); currScopeStart = variables.size(); if ((genStatements() & FLAG_STOP) == 0) { if (generateValid || random.nextInt(10)<7) { @@ -189,6 +367,7 @@ public class ScriptGenerator { this.maybeInvalid = true; } } + generateValid = oldGenerateValid; return !this.maybeInvalid; } @@ -197,7 +376,7 @@ public class ScriptGenerator { * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the last statement generated */ public int genStatements() { - var num = random.nextInt(10); // (30); + var num = random.nextInt(MAX_STATEMENTS_PER_BLOCK * (depth+1)); for (int i=0; i<num; i++) { printIndentation(); var stop = genStatement(); @@ -212,29 +391,55 @@ public class ScriptGenerator { * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement */ public int genStatement() { - return switch (random.nextInt(10)) { + return switch (random.nextInt(12)) { + case 0 -> allowStructs || generateInvalid() ? genStruct() : genAssignment(); case 1 -> genBlock(); case 2 -> genIf(); - case 3 -> genWhile(); - case 4, 5 -> genDecl(); - case 6, 7, 8 -> genAssignment(); + case 3 -> genCountedLoop(); + case 4 -> genWhile(); + case 5, 6 -> genDecl(); + case 7, 8, 9 -> genAssignment(); + case 10 -> genNullCheck(); default -> genExit(); }; } /** + * Generate a new structure + * @return 0 + */ + public int genStruct() { + var name = getStructName(); + var fields = new TypeStruct.Field[generateInvalid() ? 0 : random.nextInt(10)]; + sb.append("struct ").append(name).append(" {\n"); + indentation += INDENTATION; + for (int i=0; i<fields.length; i++) { + var fieldName = getRandomName(); + var type = generateInvalid() ? getType() : TYPE_INT; + printIndentation().append(type.name).append(" ").append(fieldName).append(";\n"); + fields[i] = new TypeStruct.Field(fieldName.toString(), type); + } + indentation -= INDENTATION; + printIndentation().append("}"); + structs.add(new TypeStruct(name, fields)); + return 0; + } + + /** * Generate a statement for if, else and while blocks. * This is special since it disallows declarations and prefers to generate blocks. * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement */ public int genStatementBlock() { + var oldAllowStructs = allowStructs; + allowStructs = false; indentation += INDENTATION; int res; if (depth == 0) { res = random.nextBoolean() ? genAssignment() : genExit(); } else { depth--; - res = switch (random.nextInt(10)) { + res = switch (random.nextInt(12)) { case 1, 2, 3, 4, 5 -> { depth++; indentation -= INDENTATION; @@ -244,13 +449,16 @@ public class ScriptGenerator { yield ret; } case 6 -> genIf(); - case 7 -> genWhile(); - case 8 -> genAssignment(); + case 7 -> genCountedLoop(); + case 8 -> genWhile(); + case 9 -> genAssignment(); + case 10 -> genNullCheck(); default -> genExit(); }; depth++; } indentation -= INDENTATION; + allowStructs = oldAllowStructs; return res; } @@ -284,18 +492,20 @@ public class ScriptGenerator { sb.append("{}"); return 0; } - depth--; - int oldCSS = currScopeStart; + var oldAllowStructs = allowStructs; + var oldCSS = currScopeStart; + allowStructs = false; currScopeStart = variables.size(); + depth--; sb.append("{\n"); indentation += INDENTATION; var stop = genStatements(); indentation -= INDENTATION; printIndentation().append("}"); - while (variables.size() > currScopeStart) - variables.removeLast(); - currScopeStart = oldCSS; + popVariables(currScopeStart); depth++; + currScopeStart = oldCSS; + allowStructs = oldAllowStructs; return stop & ~FLAG_IF_WITHOUT_ELSE; } @@ -305,7 +515,7 @@ public class ScriptGenerator { */ public int genIf() { sb.append("if("); - genExpression(); + genExpression(TYPE_INT); sb.append(") "); var stop = genStatementBlock(); if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { @@ -319,12 +529,39 @@ public class ScriptGenerator { } /** + * Generate a block with a null check + * if (nullable) { handle_nullable_as_non_nullable; } + */ + public int genNullCheck() { + var v = findVisibleVariablesMatching(va->va.declared instanceof TypeNullable); + if (v == null) return genIf(); + var n = (TypeNullable)v.declared; + boolean negate = random.nextBoolean(); + sb.append("if("); + if (negate) sb.append("!"); + sb.append(v.name).append(")"); + if (!negate) v.type = n.base; + var stop = genStatementBlock(); + v.type = n; + if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { + sb.append("\n"); + printIndentation().append("else "); + if (negate) v.type = n.base; + stop &= genStatementBlock(); + v.type = n; + } else { + stop = FLAG_IF_WITHOUT_ELSE; + } + return stop; + } + + /** * Generate a while loop. * @return 0 */ public int genWhile() { sb.append("while("); - genExpression(); + genExpression(TYPE_INT); sb.append(") "); loopDepth++; genStatementBlock(); @@ -333,15 +570,61 @@ public class ScriptGenerator { } /** + * Generate a counted loop of the form + * <code> + * { + * int varname = start_expr; + * while (varname < end_expr) loop_body; + * } + * </code> + * @return 0 + */ + public int genCountedLoop() { + if (depth == 0) return genWhile(); + var oldCSS = currScopeStart; + currScopeStart = variables.size(); + sb.append("{\n"); + indentation += INDENTATION; + String name = getVarName(); + printIndentation().append("int ").append(name).append("="); + genExpression(TYPE_INT); + sb.append(";\n"); + addVariable(name, TYPE_INT); + printIndentation().append("while(").append(name).append("<"); + genExpression(TYPE_INT); + sb.append(") {\n"); + indentation += INDENTATION; + depth--; + loopDepth++; + printIndentation().append(name).append("=").append(name).append("+"); + genExpression(TYPE_INT); + sb.append(";\n"); + genStatements(); + loopDepth--; + depth++; + indentation -= INDENTATION; + printIndentation().append("}\n"); + indentation -= INDENTATION; + printIndentation().append("}"); + popVariables(currScopeStart); + currScopeStart = oldCSS; + return 0; + } + + /** * Generate a declaration statement. * @return 0 */ public int genDecl() { - String name = getVarName(); - sb.append("int ").append(name).append("="); - genExpression(); + var type = getType(); + var name = getVarName(); + sb.append(generateInvalid() ? getRandomName() : type.name).append(" ").append(name); + if (!(type instanceof TypeNullable) || random.nextBoolean()) { + sb.append("="); + genExpression(type); + } sb.append(";"); - variables.add(name); + addVariable(name, type); return 0; } @@ -350,10 +633,30 @@ public class ScriptGenerator { * @return 0 */ public int genAssignment() { - if (variables.isEmpty()) return genDecl(); - genVariable(); + Type type; + Type declared; + if (generateInvalid()) { + sb.append(getRandomName()); + type = getType(); + declared = type; + } else { + var v = findVisibleVariablesMatching(va->true); + if (v == null) return genDecl(); + sb.append(v.name); + type = v.type; + declared = v.declared; + } + if (generateInvalid()) { + var name = getRandomName(); + sb.append(".").append(name); + declared = getType(); + } else if (type instanceof TypeStruct s && s.fields.length > 0 && random.nextBoolean()) { + var field = s.fields[random.nextInt(s.fields.length)]; + sb.append(".").append(field.name); + declared = field.type; + } sb.append("="); - genExpression(); + genExpression(declared); sb.append(";"); return 0; } @@ -363,94 +666,127 @@ public class ScriptGenerator { * @return FLAG_STOP */ public int genReturn() { + var type = getType(); sb.append("return "); - genExpression(); + genExpression(type); sb.append(";"); return FLAG_STOP; } - /** * Generate a binary expression. * This method does not care about operator precedence. */ - public void genExpression() { - var num = randLog(10); + public void genExpression(Type type) { + if (generateInvalid()) type = getType(); + if (type != TYPE_INT) { + genUnary(type); + return; + } + var num = randLog(MAX_BINARY_EXPRESSIONS_PER_EXPRESSION); while(num-->0) { - genUnary(); + genUnary(TYPE_INT); sb.append(BINARY_OP[random.nextInt(BINARY_OP.length)]); } - genUnary(); + genUnary(TYPE_INT); } /** * Generate a unary expression. */ - public void genUnary() { - var num = randLog(6); + public void genUnary(Type type) { + if (generateInvalid()) type = getType(); + if (type != TYPE_INT) { + genSuffix(type); + return; + } + var num = randLog(MAX_UNARY_EXPRESSIONS_PER_EXPRESSION); while(num-->0) { sb.append(UNARY_OP[random.nextInt(UNARY_OP.length)]); } - genPrimary(); + genSuffix(TYPE_INT); + } + + /** + * Generate + */ + public void genSuffix(Type type) { + if (generateInvalid()) type = getType(); + if (type == TYPE_INT && random.nextBoolean()) { + var t = getType(); + if (generateInvalid()) { + var field = getRandomName(); + genPrimary(t); + sb.append(".").append(field); + return; + } else if (t instanceof TypeStruct s && s.fields.length > 0) { + var field = s.fields[random.nextInt(s.fields.length)]; + if (field.type == TYPE_INT) { + genPrimary(t); + sb.append(".").append(field.name); + return; + } + } + } + genPrimary(type); } /** * Generate a primary expression. */ - public void genPrimary() { - switch (random.nextInt(10)) { - case 0: - sb.append("true"); - break; - case 1: - sb.append("false"); - break; - case 2: - if (exprDepth == 0) { - // Ensure termination of the generator. - if (random.nextBoolean()) { - genNumber(); - } else { - genVariable(); - } - } else { - sb.append("("); - exprDepth--; - genExpression(); - exprDepth++; - sb.append(")"); - } - break; - case 3, 4, 5, 6: - genNumber(); - break; - default: - genVariable(); + public void genPrimary(Type type) { + if (generateInvalid()) type = getType(); + var rand = random.nextInt(10); + if (rand == 0 && exprDepth != 0) { + sb.append("("); + exprDepth--; + genExpression(type); + exprDepth++; + sb.append(")"); + } else if (rand < 6) { + genVariable(type); + } else { + genConst(type); } } /** - * Generate a number. + * Generate a constant. */ - public void genNumber() { - sb.append(random.nextInt(100)); + public void genConst(Type type) { + if (generateInvalid()) type = getType(); + if (type == TYPE_INT) { + var rand = random.nextInt(10); + switch (rand) { + case 0 -> sb.append("true"); + case 1 -> sb.append("false"); + default -> sb.append(random.nextInt(1<<(rand-2))); + } + } else if (type instanceof TypeNullable n) { + if (random.nextBoolean()) { + sb.append("null"); + } else { + sb.append("new ").append(generateInvalid() ? getRandomName() : n.base.name); + } + } else { + sb.append("new ").append(generateInvalid() ? getRandomName() : type.name); + } } /** * Generate a variable. If there are none generate a number instead. * If the program is allowed to be invalid a random name can be generated in some cases. */ - public void genVariable() { - if (!generateValid && random.nextInt(100)>97) { + public void genVariable(Type type) { + if (generateInvalid()) { sb.append(getRandomName()); - maybeInvalid = true; return; } - if (variables.isEmpty()) { - genNumber(); - } else { - sb.append(variables.get(random.nextInt(variables.size()))); - } + if (generateInvalid()) type = getType(); + var t = type; + var v = findVisibleVariablesMatching(va->va.type.isa(t)); + if (v == null) genConst(type); + else sb.append(v.name); } } |
