From cdfb3419d6a3c3067ce3c4157b605ae4abe32b09 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Tue, 23 Apr 2024 17:23:16 +0100 Subject: Chapter 10: Structs and Memory Co-authored-by: Thalia Archibald Co-authored-by: Dibyendu Majumdar Co-authored-by: XmiliaH <45106915+XmiliaH@users.noreply.github.com> Co-authored-by: XmiliaH Co-authored-by: Robert Obkircher --- Makefile | 2 +- README.md | 2 +- docs/chapter10/10-grammar.md | 129 +++++ docs/chapter10/Makefile | 12 + docs/chapter10/README.md | 345 ++++++++++++ docs/chapter10/example1.gv | 78 +++ docs/chapter10/example1.svg | 294 ++++++++++ docs/chapter10/example2a.gv | 65 +++ docs/chapter10/example2a.svg | 249 +++++++++ docs/chapter10/example2b.gv | 56 ++ docs/chapter10/example2b.svg | 189 +++++++ docs/chapter10/example2c.gv | 53 ++ docs/chapter10/example2c.svg | 168 ++++++ docs/chapter10/lattice.drawio | 379 +++++++++++++ docs/chapter10/lattice.gv | 161 ++++++ docs/chapter10/lattice.svg | 516 ++++++++++++++++++ .../java/com/seaofnodes/simple/GraphEvaluator.java | 181 ------- .../com/seaofnodes/simple/GraphVisualizer.java | 13 +- src/main/java/com/seaofnodes/simple/IRPrinter.java | 71 ++- src/main/java/com/seaofnodes/simple/IterPeeps.java | 6 +- src/main/java/com/seaofnodes/simple/Parser.java | 238 +++++++-- src/main/java/com/seaofnodes/simple/Utils.java | 26 + .../java/com/seaofnodes/simple/node/AddNode.java | 2 +- .../java/com/seaofnodes/simple/node/BoolNode.java | 18 +- .../java/com/seaofnodes/simple/node/CastNode.java | 38 ++ .../com/seaofnodes/simple/node/ConstantNode.java | 4 - .../java/com/seaofnodes/simple/node/IfNode.java | 3 +- .../java/com/seaofnodes/simple/node/LoadNode.java | 78 +++ .../java/com/seaofnodes/simple/node/LoopNode.java | 2 + .../java/com/seaofnodes/simple/node/MemOpNode.java | 40 ++ .../java/com/seaofnodes/simple/node/MinusNode.java | 8 +- .../java/com/seaofnodes/simple/node/NewNode.java | 40 ++ src/main/java/com/seaofnodes/simple/node/Node.java | 97 ++-- .../java/com/seaofnodes/simple/node/NotNode.java | 24 +- .../java/com/seaofnodes/simple/node/PhiNode.java | 46 +- .../java/com/seaofnodes/simple/node/ProjNode.java | 12 +- .../com/seaofnodes/simple/node/RegionNode.java | 45 +- .../com/seaofnodes/simple/node/ReturnNode.java | 13 +- .../java/com/seaofnodes/simple/node/ScopeNode.java | 87 ++- .../java/com/seaofnodes/simple/node/StartNode.java | 56 +- .../java/com/seaofnodes/simple/node/StopNode.java | 11 +- .../java/com/seaofnodes/simple/node/StoreNode.java | 70 +++ .../java/com/seaofnodes/simple/node/SubNode.java | 8 + .../java/com/seaofnodes/simple/type/Field.java | 60 +++ src/main/java/com/seaofnodes/simple/type/Type.java | 76 ++- .../com/seaofnodes/simple/type/TypeInteger.java | 27 + .../java/com/seaofnodes/simple/type/TypeMem.java | 64 +++ .../com/seaofnodes/simple/type/TypeMemPtr.java | 78 +++ .../com/seaofnodes/simple/type/TypeStruct.java | 121 +++++ .../java/com/seaofnodes/simple/type/TypeTuple.java | 30 +- .../java/com/seaofnodes/simple/Chapter01Test.java | 4 +- .../java/com/seaofnodes/simple/Chapter03Test.java | 2 +- .../java/com/seaofnodes/simple/Chapter05Test.java | 12 +- .../java/com/seaofnodes/simple/Chapter06Test.java | 8 +- .../java/com/seaofnodes/simple/Chapter07Test.java | 6 +- .../java/com/seaofnodes/simple/Chapter08Test.java | 5 +- .../java/com/seaofnodes/simple/Chapter09Test.java | 11 +- .../java/com/seaofnodes/simple/Chapter10Test.java | 404 ++++++++++++++ .../java/com/seaofnodes/simple/SchedulerTest.java | 74 +++ src/test/java/com/seaofnodes/simple/TypeTest.java | 145 +++++ .../com/seaofnodes/simple/evaluator/Evaluator.java | 209 ++++++++ .../com/seaofnodes/simple/evaluator/Scheduler.java | 592 +++++++++++++++++++++ .../java/com/seaofnodes/simple/fuzzer/Fuzzer.java | 53 +- .../com/seaofnodes/simple/fuzzer/FuzzerUtils.java | 10 +- .../java/com/seaofnodes/simple/fuzzer/Reducer.java | 92 ++-- .../seaofnodes/simple/fuzzer/ScriptGenerator.java | 498 ++++++++++++++--- 66 files changed, 5998 insertions(+), 518 deletions(-) create mode 100644 docs/chapter10/10-grammar.md create mode 100644 docs/chapter10/Makefile create mode 100644 docs/chapter10/README.md create mode 100644 docs/chapter10/example1.gv create mode 100644 docs/chapter10/example1.svg create mode 100644 docs/chapter10/example2a.gv create mode 100644 docs/chapter10/example2a.svg create mode 100644 docs/chapter10/example2b.gv create mode 100644 docs/chapter10/example2b.svg create mode 100644 docs/chapter10/example2c.gv create mode 100644 docs/chapter10/example2c.svg create mode 100644 docs/chapter10/lattice.drawio create mode 100644 docs/chapter10/lattice.gv create mode 100644 docs/chapter10/lattice.svg delete mode 100644 src/main/java/com/seaofnodes/simple/GraphEvaluator.java create mode 100644 src/main/java/com/seaofnodes/simple/node/CastNode.java create mode 100644 src/main/java/com/seaofnodes/simple/node/LoadNode.java create mode 100644 src/main/java/com/seaofnodes/simple/node/MemOpNode.java create mode 100644 src/main/java/com/seaofnodes/simple/node/NewNode.java create mode 100644 src/main/java/com/seaofnodes/simple/node/StoreNode.java create mode 100644 src/main/java/com/seaofnodes/simple/type/Field.java create mode 100644 src/main/java/com/seaofnodes/simple/type/TypeMem.java create mode 100644 src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java create mode 100644 src/main/java/com/seaofnodes/simple/type/TypeStruct.java create mode 100644 src/test/java/com/seaofnodes/simple/Chapter10Test.java create mode 100644 src/test/java/com/seaofnodes/simple/SchedulerTest.java create mode 100644 src/test/java/com/seaofnodes/simple/TypeTest.java create mode 100644 src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java create mode 100644 src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java diff --git a/Makefile b/Makefile index b5c21b38..c7724bc7 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,7 @@ tests: $(default_targets) @$(JVM) org.junit.runner.JUnitCore $(test_cp) fuzzer: $(default_targets) - @echo "testing " $(test_cp) + @echo "fuzzing " $(test_cp) @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap diff --git a/README.md b/README.md index 2d670b17..bad946a8 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The Simple language will be styled after a subset of C or Java * [Chapter 7](docs/chapter07/README.md): `while` statement. Looping construct - eager phi approach. * [Chapter 8](docs/chapter08/README.md): Looping construct continued, lazy phi creation, `break` and `continue` statements. * [Chapter 9](docs/chapter09/README.md): Global Value Numbering. Iterative peepholes to fixpoint. Worklists. -* Chapter 10: User defined Struct types. Memory effects: general memory edges in SSA. Equivalence class aliasing. Null pointer analysis. Peephole optimization around load-after-store/store-after-store. +* [Chapter 10](docs/chapter10/README.md): User defined Struct types. Memory effects: general memory edges in SSA. Equivalence class aliasing. Null pointer analysis. Peephole optimization around load-after-store/store-after-store. * Chapter 11: Global Code Motion - Scheduling. * Chapter 12: Float type. * Chapter 13: Nested references in Structs. diff --git a/docs/chapter10/10-grammar.md b/docs/chapter10/10-grammar.md new file mode 100644 index 00000000..10c80426 --- /dev/null +++ b/docs/chapter10/10-grammar.md @@ -0,0 +1,129 @@ +# Grammar for Chapter 10 + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | structDeclaration + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatment + | metaStatement + ; + +field + : 'int' IDENTIFIER ';' + ; + +fields + : field+ + ; + +structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + +metaStatement + : '#showGraph' ';' + ; + +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/chapter10/Makefile b/docs/chapter10/Makefile new file mode 100644 index 00000000..12659b8f --- /dev/null +++ b/docs/chapter10/Makefile @@ -0,0 +1,12 @@ +GV := $(wildcard *.gv) +SVG := $(patsubst %.gv,%.svg,$(GV)) + +.PHONY: all +all: $(SVG) + +%.svg: %.gv + dot -Tsvg $< > $@ + +.PHONY: clean +clean: + @rm -f $(SVG) diff --git a/docs/chapter10/README.md b/docs/chapter10/README.md new file mode 100644 index 00000000..ffac4e15 --- /dev/null +++ b/docs/chapter10/README.md @@ -0,0 +1,345 @@ +# Chapter 10: Structs and Memory + +In this chapter: + +* We add user defined struct types in the Simple language. +* We introduce the concept of Memory, Memory Pointers and Aliasing. +* We add new nodes to represent operations on memory - such as creating new instance of a struct, storing and loading struct fields. + +Here is the [complete language grammar](10-grammar.md) for this chapter. + +You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linear-chapter10) in a linear Git revision history on the [linear](https://github.com/SeaOfNodes/Simple/tree/linear) branch and [compare](https://github.com/SeaOfNodes/Simple/compare/linear-chapter09...linear-chapter10) it to the previous chapter. + +## Memory + +In previous chapters, we dealt with scalar values that fit into a single +machine register. We now introduce aggregate values of `struct` type; these +have reference semantics and thus do not behave like register values, even if +the struct would fit into a register. To support values of such a type, we +need the concept of `Memory`. + +The core ideas regarding `Memory` are: + +* The program starts and stops with a single `Memory` value that encapsulates + the whole of available memory; `Start` produces memory and `Return` consumes + memory. + +* Each `struct` and field carves up this memory into "alias classes", a unique + class for each combination of struct and field. + +* Memory in different alias classes *never* aliases; memory in the same alias + class *always* aliases. This is sometimes called "equivalence class" + aliasing. + +* Equivalence class aliasing can be directly represented in the Sea of Nodes, + as such we will not use any other side-structure for aliasing. This also + means with normal peepholes we can do alias-based optimizations such as + load/store bypassing or load forwarding without needing to consult any side + structure, or make alias tracking modifications when we alter the graph. + + +### Alias Classes + +Aliasing describes a situation in which a data location in memory can be +accessed through multiple symbolic names (variables) in the program. Modifying +the data through one name implicitly modifies the values associated with all +the aliased names. + +Data accesses to the same alias class (struct+field) are serialized as defined +by the source program; data in other classes is allowed to be accessed in +parallel. + +Simple, like Java, is a strongly typed language. Therefore, two variables that +have incompatible types cannot possibly be names to the same location in +memory. We can use type information to determine which symbols alias, +i.e., potentially reference the same location in memory. We +divide/slice all memory into disjoint alias classes. + +We implement the "alias class" as a unique integer ID assigned to each field in +a struct, where distinct struct types have their own set of ids. Thus, every +field in every struct type is a new alias class. In later chapters as we +implement arrays and subtyping, our strategy for assigning alias classes will +be enhanced. + +For background information and overview of type based alias analysis, please +refer to the paper `Type Based Alias Analysis`.[^1] Our approach is a form of +TBAA similar to the "FieldTypeDecl" algorithm described in the paper. We do +not track aliasing by instance of a Struct type. + + +### Implementation of Alias Classes + +For example, suppose we have declared two struct types like so: + +``` +struct Vec2D { int x; int y; } +struct Vec3D { int x; int y; int z; } +``` + +Then we can imagine assigning following alias class IDs + +| Struct type | Field | Alias | +| ----------- | ----- | ----- | +| Vec2D | x | 1 | +| Vec2D | y | 2 | +| Vec3D | x | 3 | +| Vec3D | y | 4 | +| Vec3D | z | 5 | + +So at this point we have sliced memory into 5 distinct alias classes. +Note that the `x` and `y` in `Vec2D` do not alias `x` and `y` in `Vec3D`. + +In this chapter we do not have inheritance or sub-typing. But if we had subtyping and `Vec3D` +was a subtype of `Vec2D` then `x` and `y` would alias and would be given the same alias class. + +## Extensions to Intermediate Representation + +We add following new Node types to support memory operations: + +| Node Name | Type | Description | Inputs | Value | +|-----------|------|----------------------------------|-----------------------------------------------------------|----------------------------------------| +| New | Mem | Create ptr to new object | Control, Struct type | Ptr value | +| Store | Mem | Stores a value in a struct field | Memory slice (aliased by struct+field), Ptr, Field, Value | Memory slice (aliased by struct+field) | +| Load | Mem | Loads a value from a field | Memory slice (aliased by struct+field), Ptr, Field | Value loaded | +| Cast | Data | Upcasts a value | Control, Data, type | Upcast value | + + +* New takes the current control as an input so that it is pinned correctly in + the control flow. Conceptually the control input is also a proxy for all + memory that originates from the Start node. + +* Above, "Ptr" refers to the base address of the allocated object. Ptrs can represent different values in loops. +* A "Memory Slice" represents a slice of memory where all stores and loads alias. Different slices do not alias. + +Additionally, the following Node types will be enhanced: + +| Node Name | Type | Changes | +|-----------|---------|------------------------------------------------------------------| +| Start | Control | Start will produce the initial Memory projections, one per slice | +| Return | Control | Will merge all memory slices | + + +## Enhanced Type Lattice + +The Type Lattice for Simple has a major revision in this chapter. + +![Graph1](./lattice.svg) + +Within the Type Lattice, we now have the following type domains: + +* Control type - represents control flow +* Integer type - Integer values, with a Top, Bottom and constants +* Pointer type (new) - Represents a pointer to a struct type + * We use the prefix `*` to mean pointer-to. Thus `*S1` means pointer to + `S1`, `*$TOP` means pointer to `$TOP`. + * `?` suffix represents the union of a pointer to some type and `null`. + * The pointer lattice has only 2 entries: null or not-null. In both cases a struct lattice element is + included, and can be any member of the struct lattice. + * `null` is a pointer to non-existent memory object, i.e. `*$TOP?`, and is a + nullable. Notice that the null part of the type is low in the lattice, + while the struct part is the highest struct. + * `*void` is a synonym for `*$BOT` - i.e. it represents a non-null pointer to + all possible struct types, akin to `void *` in C except not null. It is + the inverse or *dual* of `null`, and the null part is high in the pointer + lattice, while the struct part is the lowest struct. + * `null` pointer evaluates to False and non-null pointers evaluate to True, as in C. + +* Struct type (new) - + Represents user defined struct types, a struct type is allowed to have + members of Integer type only in this chapter, later chapters will expand + this. + * `⊤:struct` represents local Top for struct type; all we know about the type + is that it is a struct but, we do not know if it is a specific struct, or + all possible structs. In the code, we use a struct with no fields named `$TOP`. + * `⊥:struct` represents local Bottom for struct type; we definitely know the + value can take all possible struct types. In the code, we uise a struct + with no fields named `$BOT`. +* Memory type (new) - Represents memory and memory slices + * `⊤:mem` - Nothing known about a slice; in the code we use `MEM#TOP`. + * `#n` - refers to a memory slice, and `n` is the alias class. + * `⊥:mem` - all of memory; in the code we use `MEM#BOT`. +* Tuple type - when a Node results in a collection of values + +We make use of following operations on the lattice. + +* The `meet` operation takes two types and computes the greatest lower bound + type. While not true for all lattices, in the our lattice this can be + thought of as set union (ORing) of its input types (as sets). + * Example, meet of `*S1` and `*S2` results in `*void` - there is no lattice + element for exactly both `{*S1,*S2}`, so we drop down to the nearest + element containing both (and maybe more), which is `*void`. + * Meet of `*S1` and pointer `null` results in `*S1?` + * Meet of `*void` and `null` results in `*$BOT?`; bear in mind that `*void` is a synonym for `*$BOT`. + * Meet of `1` and `2` results in `Int Bot`. + * Meet of `#1` and `#2` results in `MEM#BOT`. +* The `dual` operation is symmetric across the lattice centerline. Types directly on the centerline are their own dual. + * Find the corresponding node of the lattice after inverting the lattice. + * Thus, dual of `Top` is `Bot`. + * The dual of `*$TOP` is `*$BOT?`. + * For structs, the dual is obtained by computing the dual of each struct + member. Thus, dual of `*S1` is not `*S1?`, it is some `*S1?` with dualed + fields. + +* The `join` operation takes two types and computes the least upper bound type. + Similar to `meet` this can thought of as set intersection (ANDing) of its input types. + Because our lattice structure is symmetric and complete, we compute `join` with a well known identity: + `JOIN(x,y) === MEET(x.dual,y.dual).dual` + * Example, join of `*S1` and `*S2` results in `*$TOP`. + * Join of `1` and `2` results in `Int Top`. + * Join of `*void` and `Null` results in `*$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 is done using a `Cast` op with the type `*void`. This does a + lattice JOIN of the original type and `*void`. 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()`. + +## Memory Operations Example + +This is a simple example illustrating how loads and stores are ordered based on aliasing. + +```java +struct Vec2D { int x; int y; } + +Vec2D v = new Vec2D; +v.x = 1; +if (arg) + v.y = 2; +else + v.y = 3; +return v; +``` + +The graph from above will have the shape: + +![Graph1](./example1.svg) + +The graph visual has the following enhancements: + +* The memory edges are shown in green and blue. +* The green edges occur on New nodes and anchor the ptr to a control flow node. +* The blue edges show the threading of a memory op via the equivalence aliasing. + +## Additional Peephole Optimizations + +The equivalence aliasing makes it possible to optimize following scenarios: + +* Store followed by another Store to the same field inside an object; the first Store is redundant if there are no other references to the first Store +* Load followed by a Store to a field inside an Object; the Load can be eliminated and the input value of the Store passed through. + +Both of these examples are illustrated in the example below: + +```java +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; +``` + +Before the peephole optimizations, we get: + +![Graph2a](./example2a.svg) + +After enabling Store-Store peephole we get: + +![Graph2b](./example2b.svg) + +After load-after-store elision: + +![Graph2c](./example2c.svg) + +Note that at this stage the `bar` object is dead therefore the allocation of bar can be dropped. + + +[^1]: Diwan A., McKinley K.S., Moss J.E.B. (1998). + Type Based Alias Analysis. diff --git a/docs/chapter10/example1.gv b/docs/chapter10/example1.gv new file mode 100644 index 00000000..cbb12cb5 --- /dev/null +++ b/docs/chapter10/example1.gv @@ -0,0 +1,78 @@ +digraph chapter10 { +/* +struct Vector2D { int x; int y; } +Vector2D v = new Vector2D; +v.x = 1; +if (arg) + v.y = 2; +else + v.y = 3; +return v; + +*/ + rankdir=BT; + concentrate="true"; + compound="true"; + subgraph cluster_Nodes { + Start2 [ shape=plaintext label=< + + + +
Start
+ + +
$ctrlarg$2$3
+
> + ]; + Stop3 [ shape=box style=filled fillcolor=yellow label="Stop" ]; + new8 [ label="new Vector2D" ]; + Con_9 [ label="#0" ]; + Store11 [ label=".y =" ]; + Con_12 [ label="#1" ]; + Store13 [ label=".x =" ]; + If14 [ shape=plaintext label=< + + + +
If
+ + +
TrueFalse
+
> + ]; + Con_18 [ label="#2" ]; + Store19 [ label=".y =" ]; + Con_22 [ label="#3" ]; + Store23 [ label=".y =" ]; + Region24 [ shape=box style=filled fillcolor=yellow label="Region" ]; + Phi_326 [ style=filled fillcolor=lightyellow label="φ_$3" ]; + Return27 [ shape=box style=filled fillcolor=yellow label="Return" ]; + { rank=same; Region24;Phi_326;} + } + edge [ fontname=Helvetica, fontsize=8 ]; + Stop3 -> Return27[taillabel=0 color=red]; + new8 -> Start2:p0[taillabel=0 color=green]; + Store11 -> Start2:p3[taillabel=1 color=blue]; + Store11 -> new8[taillabel=2]; + Store11 -> Con_9[taillabel=3]; + Store13 -> Start2:p2[taillabel=1 color=blue]; + Store13 -> new8[taillabel=2]; + Store13 -> Con_12[taillabel=3]; + If14 -> Start2:p0[taillabel=0 color=red]; + If14 -> Start2:p1[taillabel=1]; + Store19 -> Store11[taillabel=1 color=blue]; + Store19 -> new8[taillabel=2]; + Store19 -> Con_18[taillabel=3]; + Store23 -> Store11[taillabel=1 color=blue]; + Store23 -> new8[taillabel=2]; + Store23 -> Con_22[taillabel=3]; + Region24 -> If14:p0[taillabel=1 color=red]; + Region24 -> If14:p1[taillabel=2 color=red]; + Phi_326 -> Region24 [style=dotted taillabel=0]; + Phi_326 -> Store19[taillabel=1 color=blue]; + Phi_326 -> Store23[taillabel=2 color=blue constraint=false]; + Return27 -> Region24[taillabel=0 color=red]; + Return27 -> new8[taillabel=1]; + Return27 -> Store13[taillabel=2 color=blue]; + Return27 -> Phi_326[taillabel=3 color=blue]; +} diff --git a/docs/chapter10/example1.svg b/docs/chapter10/example1.svg new file mode 100644 index 00000000..6f493e7e --- /dev/null +++ b/docs/chapter10/example1.svg @@ -0,0 +1,294 @@ + + + + + + +chapter10 + + +cluster_Nodes + + + + +Start2 + + +Start + + + +$ctrl + +arg + +$2 + +$3 + + + +Stop3 + +Stop + + + +Return27 + +Return + + + +Stop3->Return27 + + +0 + + + +new8 + +new Vector2D + + + +new8->Start2:p0 + + +0 + + + +Con_9 + +#0 + + + +Store11 + +.y = + + + +Store11->Start2:p3 + + +1 + + + +Store11->new8 + + +2 + + + +Store11->Con_9 + + +3 + + + +Con_12 + +#1 + + + +Store13 + +.x = + + + +Store13->Start2:p2 + + +1 + + + +Store13->new8 + + +2 + + + +Store13->Con_12 + + +3 + + + +If14 + + +If + + + +True + + +False + + + +If14->Start2:p0 + + +0 + + + +Con_18 + +#2 + + + +Store19 + +.y = + + + +Store19->new8 + + + +2 + + + +Store19->Store11 + + +1 + + + +Store19->Con_18 + + +3 + + + +Con_22 + +#3 + + + +Store23 + +.y = + + + +Store23->new8 + +2 + + + +Store23->Store11 + + +1 + + + +Store23->Con_22 + + +3 + + + +Region24 + +Region + + + +Region24->If14:p0 + + +1 + + + +Region24->If14:p1 + + +2 + + + +Phi_326 + +φ_$3 + + + +Phi_326->Store19 + + +1 + + + +Phi_326->Store23 + + +2 + + + +Phi_326->Region24 + + +0 + + + +Return27->new8 + + +1 + + + +Return27->Store13 + + +2 + + + +Return27->Region24 + + +0 + + + +Return27->Phi_326 + + +3 + + + diff --git a/docs/chapter10/example2a.gv b/docs/chapter10/example2a.gv new file mode 100644 index 00000000..23f31bf8 --- /dev/null +++ b/docs/chapter10/example2a.gv @@ -0,0 +1,65 @@ +digraph chapter10 { +/* +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; + +*/ + rankdir=BT; + concentrate="true"; + compound="true"; + subgraph cluster_Nodes { + Start2 [ shape=plaintext label=< + + + +
Start
+ + +
$ctrl$2$3$4
+
> + ]; + Stop3 [ shape=box style=filled fillcolor=yellow label="Stop" ]; + new10 [ label="new Bar" ]; + Con_11 [ label="#0" ]; + Store12 [ label=".a =" ]; + Store13 [ label=".b =" ]; + Con_14 [ label="#1" ]; + Store15 [ label=".a =" ]; + Con_16 [ label="#2" ]; + Store17 [ label=".a =" ]; + Load18 [ label=".a" ]; + Return19 [ shape=box style=filled fillcolor=yellow label="Return" ]; + } + edge [ fontname=Helvetica, fontsize=8 ]; + Stop3 -> Return19[taillabel=0 color=red]; + new10 -> Start2:p0[taillabel=0 color=green]; + Store12 -> Start2:p2[taillabel=1 color=blue]; + Store12 -> new10[taillabel=2]; + Store12 -> Con_11[taillabel=3]; + Store13 -> Start2:p3[taillabel=1 color=blue]; + Store13 -> new10[taillabel=2]; + Store13 -> Con_11[taillabel=3]; + Store15 -> Store12[taillabel=1 color=blue]; + Store15 -> new10[taillabel=2]; + Store15 -> Con_14[taillabel=3]; + Store17 -> Store15[taillabel=1 color=blue]; + Store17 -> new10[taillabel=2]; + Store17 -> Con_16[taillabel=3]; + Load18 -> Store17[taillabel=1 color=blue]; + Load18 -> new10[taillabel=2]; + Return19 -> Start2:p0[taillabel=0 color=red]; + Return19 -> Load18[taillabel=1]; + Return19 -> Store17[taillabel=2 color=blue]; + Return19 -> Store13[taillabel=3 color=blue]; + Return19 -> Start2:p4[taillabel=4 color=blue]; +} diff --git a/docs/chapter10/example2a.svg b/docs/chapter10/example2a.svg new file mode 100644 index 00000000..9f069ad4 --- /dev/null +++ b/docs/chapter10/example2a.svg @@ -0,0 +1,249 @@ + + + + + + +chapter10 + + +cluster_Nodes + + + + +Start2 + + +Start + + + +$ctrl + +$2 + +$3 + +$4 + + + +Stop3 + +Stop + + + +Return19 + +Return + + + +Stop3->Return19 + + +0 + + + +new10 + +new Bar + + + +new10->Start2:p0 + + +0 + + + +Con_11 + +#0 + + + +Store12 + +.a = + + + +Store12->Start2:p2 + + +1 + + + +Store12->new10 + + +2 + + + +Store12->Con_11 + + +3 + + + +Store13 + +.b = + + + +Store13->Start2:p3 + + +1 + + + +Store13->new10 + + +2 + + + +Store13->Con_11 + + +3 + + + +Con_14 + +#1 + + + +Store15 + +.a = + + + +Store15->new10 + + +2 + + + +Store15->Store12 + + +1 + + + +Store15->Con_14 + + +3 + + + +Con_16 + +#2 + + + +Store17 + +.a = + + + +Store17->new10 + +2 + + + +Store17->Store15 + + +1 + + + +Store17->Con_16 + + +3 + + + +Load18 + +.a + + + +Load18->new10 + + + +2 + + + +Load18->Store17 + + +1 + + + +Return19->Start2:p0 + + +0 + + + +Return19->Start2:p4 + + +4 + + + +Return19->Store13 + + + +3 + + + +Return19->Store17 + + + +2 + + + +Return19->Load18 + + +1 + + + diff --git a/docs/chapter10/example2b.gv b/docs/chapter10/example2b.gv new file mode 100644 index 00000000..d732e874 --- /dev/null +++ b/docs/chapter10/example2b.gv @@ -0,0 +1,56 @@ +digraph chapter10 { +/* +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; + +*/ + rankdir=BT; + concentrate="true"; + compound="true"; + subgraph cluster_Nodes { + Start2 [ shape=plaintext label=< + + + +
Start
+ + +
$ctrl$2$3$4
+
> + ]; + Stop3 [ shape=box style=filled fillcolor=yellow label="Stop" ]; + new10 [ label="new Bar" ]; + Con_11 [ label="#0" ]; + Store13 [ label=".b =" ]; + Con_16 [ label="#2" ]; + Store17 [ label=".a =" ]; + Load18 [ label=".a" ]; + Return19 [ shape=box style=filled fillcolor=yellow label="Return" ]; + } + edge [ fontname=Helvetica, fontsize=8 ]; + Stop3 -> Return19[taillabel=0 color=red]; + new10 -> Start2:p0[taillabel=0 color=green]; + Store13 -> Start2:p3[taillabel=1 color=blue]; + Store13 -> new10[taillabel=2]; + Store13 -> Con_11[taillabel=3]; + Store17 -> Start2:p2[taillabel=1 color=blue]; + Store17 -> new10[taillabel=2]; + Store17 -> Con_16[taillabel=3]; + Load18 -> Store17[taillabel=1 color=blue]; + Load18 -> new10[taillabel=2]; + Return19 -> Start2:p0[taillabel=0 color=red]; + Return19 -> Load18[taillabel=1]; + Return19 -> Store17[taillabel=2 color=blue]; + Return19 -> Store13[taillabel=3 color=blue]; + Return19 -> Start2:p4[taillabel=4 color=blue]; +} diff --git a/docs/chapter10/example2b.svg b/docs/chapter10/example2b.svg new file mode 100644 index 00000000..1ecf4de7 --- /dev/null +++ b/docs/chapter10/example2b.svg @@ -0,0 +1,189 @@ + + + + + + +chapter10 + + +cluster_Nodes + + + + +Start2 + + +Start + + + +$ctrl + +$2 + +$3 + +$4 + + + +Stop3 + +Stop + + + +Return19 + +Return + + + +Stop3->Return19 + + +0 + + + +new10 + +new Bar + + + +new10->Start2:p0 + + +0 + + + +Con_11 + +#0 + + + +Store13 + +.b = + + + +Store13->Start2:p3 + + +1 + + + +Store13->new10 + + +2 + + + +Store13->Con_11 + + +3 + + + +Con_16 + +#2 + + + +Store17 + +.a = + + + +Store17->Start2:p2 + + +1 + + + +Store17->new10 + + +2 + + + +Store17->Con_16 + + +3 + + + +Load18 + +.a + + + +Load18->new10 + + +2 + + + +Load18->Store17 + + +1 + + + +Return19->Start2:p0 + + + +0 + + + +Return19->Start2:p4 + + +4 + + + +Return19->Store13 + + + +3 + + + +Return19->Store17 + + +2 + + + +Return19->Load18 + + +1 + + + diff --git a/docs/chapter10/example2c.gv b/docs/chapter10/example2c.gv new file mode 100644 index 00000000..76db03bb --- /dev/null +++ b/docs/chapter10/example2c.gv @@ -0,0 +1,53 @@ +digraph chapter10 { +/* +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; + +*/ + rankdir=BT; + concentrate="true"; + compound="true"; + subgraph cluster_Nodes { + Con_16 [ label="#2" ]; + Store17 [ label=".a =" ]; + Start2 [ shape=plaintext label=< + + + +
Start
+ + +
$ctrl$2$3$4
+
> + ]; + Return19 [ shape=box style=filled fillcolor=yellow label="Return" ]; + Stop3 [ shape=box style=filled fillcolor=yellow label="Stop" ]; + new10 [ label="new Bar" ]; + Con_11 [ label="#0" ]; + Store13 [ label=".b =" ]; + } + edge [ fontname=Helvetica, fontsize=8 ]; + Store17 -> Start2:p2[taillabel=1 color=blue]; + Store17 -> new10[taillabel=2]; + Store17 -> Con_16[taillabel=3]; + Return19 -> Start2:p0[taillabel=0 color=red]; + Return19 -> Con_16[taillabel=1]; + Return19 -> Store17[taillabel=2 color=blue]; + Return19 -> Store13[taillabel=3 color=blue]; + Return19 -> Start2:p4[taillabel=4 color=blue]; + Stop3 -> Return19[taillabel=0 color=red]; + new10 -> Start2:p0[taillabel=0 color=green]; + Store13 -> Start2:p3[taillabel=1 color=blue]; + Store13 -> new10[taillabel=2]; + Store13 -> Con_11[taillabel=3]; +} diff --git a/docs/chapter10/example2c.svg b/docs/chapter10/example2c.svg new file mode 100644 index 00000000..d70660a2 --- /dev/null +++ b/docs/chapter10/example2c.svg @@ -0,0 +1,168 @@ + + + + + + +chapter10 + + +cluster_Nodes + + + + +Con_16 + +#2 + + + +Store17 + +.a = + + + +Store17->Con_16 + + +3 + + + +Start2 + + +Start + + + +$ctrl + +$2 + +$3 + +$4 + + + +Store17->Start2:p2 + + +1 + + + +new10 + +new Bar + + + +Store17->new10 + + +2 + + + +Return19 + +Return + + + +Return19->Con_16 + + +1 + + + +Return19->Store17 + + +2 + + + +Return19->Start2:p0 + + + +0 + + + +Return19->Start2:p4 + + +4 + + + +Store13 + +.b = + + + +Return19->Store13 + + +3 + + + +Stop3 + +Stop + + + +Stop3->Return19 + + +0 + + + +new10->Start2:p0 + + +0 + + + +Con_11 + +#0 + + + +Store13->Start2:p3 + + +1 + + + +Store13->new10 + + +2 + + + +Store13->Con_11 + + +3 + + + diff --git a/docs/chapter10/lattice.drawio b/docs/chapter10/lattice.drawio new file mode 100644 index 00000000..cf20e9f3 --- /dev/null +++ b/docs/chapter10/lattice.drawio @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/chapter10/lattice.gv b/docs/chapter10/lattice.gv new file mode 100644 index 00000000..7723e2a2 --- /dev/null +++ b/docs/chapter10/lattice.gv @@ -0,0 +1,161 @@ +digraph Lattice { + compound = true; + // Define nodes + splines=false; + node [shape=circle, style=filled]; + nodesep=0.6; + + TOP [label=<⊤>, fillcolor=black, fontcolor=white]; + BOT [label=<⊥>, fillcolor=black, fontcolor=white]; + + // ---------------------- + // Control + XCTRL [label=<⊤:ctrl>, fillcolor=yellow, shape=oval]; + CTRL [label=<⊥:ctrl>, fillcolor=yellow, shape=oval]; + + TOP -> XCTRL; + XCTRL -> CTRL; + CTRL -> BOT; + + // ---------------------- + // Integers + INT_TOP [label=<⊤:int>, fillcolor=lightblue, shape=oval]; + INT_BOT [label=<⊥:int>, fillcolor=lightblue, shape=oval]; + + INT_MINDOTS [label="...", fillcolor=lightblue, shape=oval]; + INT_ZERO [label="0" , fillcolor=lightblue]; + INT_MAXDOTS [label="...", fillcolor=lightblue, shape=oval]; + + TOP -> INT_TOP; + INT_TOP -> INT_MINDOTS; + INT_TOP -> INT_ZERO; + INT_TOP -> INT_MAXDOTS; + INT_MINDOTS -> INT_BOT; + INT_ZERO -> INT_BOT; + INT_MAXDOTS -> INT_BOT; + INT_BOT -> BOT; + + INT_MINDOTS -> INT_ZERO -> INT_MAXDOTS [style=invis]; + + // ---------------------- + // Pointers, just a boolean nil/not and a struct + PTR [shape=none, fillcolor=none, label=< + + + +
*ptr
struct
+ >]; + PTR0 [shape=none, fillcolor=none, label=< + + + +
*ptr?
struct
+ >]; + + TOP -> PTR; + PTR -> PTR0; + PTR0 -> BOT; + + // ---------------------- + // Structs + STRUCT_TOP [label=<⊤:struct>, fillcolor=lightgreen, shape=oval]; + STRUCT_BOT [label=<⊥:struct>, fillcolor=lightgreen, shape=oval]; + + STRUCT_S1 [shape=none, fillcolor=none, label=< + + +
fieldint/ptr
+ >]; + STRUCT_S2 [shape=none, fillcolor=none, label=< + + + +
fieldint/ptr
fieldint/ptr
+ >]; + STRUCT_SN [shape=none, fillcolor=none, label=< + + + + +
fieldint/ptr
... int/ptr
fieldint/ptr
+ >]; + + + TOP -> STRUCT_TOP; + STRUCT_TOP -> STRUCT_S1; + STRUCT_TOP -> STRUCT_S2; + STRUCT_TOP -> STRUCT_SN; + STRUCT_S1 -> STRUCT_BOT; + STRUCT_S2 -> STRUCT_BOT; + STRUCT_SN -> STRUCT_BOT; + STRUCT_BOT -> BOT; + + STRUCT_S1 -> STRUCT_S2 -> STRUCT_SN [style=invis]; + + // ---------------------- + // Memory + MEM_TOP [label=<⊤:mem>, fillcolor=blue, fontcolor=white, shape=oval]; + MEM_BOT [label=<⊥:mem>, fillcolor=blue, fontcolor=white, shape=oval]; + MEM_ALIAS1 [label="#1" , fillcolor=blue, fontcolor=white]; + MEM_ALIASDOTS [label="..." , fillcolor=blue, fontcolor=white, shape=oval]; + MEM_ALIASN [label="#n" , fillcolor=blue, fontcolor=white]; + + TOP -> MEM_TOP; + MEM_TOP -> MEM_ALIAS1; + MEM_TOP -> MEM_ALIASDOTS; + MEM_TOP -> MEM_ALIASN; + MEM_ALIAS1 -> MEM_BOT; + MEM_ALIASDOTS -> MEM_BOT; + MEM_ALIASN -> MEM_BOT; + MEM_BOT -> BOT; + + MEM_ALIAS1 -> MEM_ALIASDOTS -> MEM_ALIASN [style=invis]; + + // ---------------------- + // Tuples + + TUPLE_TOP [label=<⊤:tuple>, fillcolor=pink, shape=oval]; + TUPLE_BOT [label=<⊥:tuple>, fillcolor=pink, shape=oval]; + + TUPLE1 [shape=none, fillcolor=none, label=< + + + +
Tuple
type
+ >]; + TUPLE2 [shape=none, fillcolor=none, label=< + + + + +
Tuple
type
type
+ >]; + TUPLEN [shape=none, fillcolor=none, label=< + + + + + +
Tuple
type
...
type
+ >]; + + + TOP -> TUPLE_TOP; + TUPLE_TOP -> TUPLE1; + TUPLE_TOP -> TUPLE2; + TUPLE_TOP -> TUPLEN; + TUPLE1 -> TUPLE_BOT; + TUPLE2 -> TUPLE_BOT; + TUPLEN -> TUPLE_BOT; + TUPLE_BOT -> BOT; + + // ---------------------- + // Additional customization + {rank=same; TOP;} + {rank=same; INT_TOP; XCTRL; STRUCT_TOP; MEM_TOP; TUPLE_TOP} + {rank=same; INT_MINDOTS; INT_ZERO; INT_MAXDOTS; + STRUCT_S1; STRUCT_S2; STRUCT_SN; + MEM_ALIAS1; MEM_ALIASDOTS; MEM_ALIASN;} + {rank=same; INT_BOT; CTRL; STRUCT_BOT; MEM_BOT; TUPLE_BOT} + {rank=same; BOT;} +} diff --git a/docs/chapter10/lattice.svg b/docs/chapter10/lattice.svg new file mode 100644 index 00000000..9eb36bbe --- /dev/null +++ b/docs/chapter10/lattice.svg @@ -0,0 +1,516 @@ + + + + + + +Lattice + + + +TOP + +⊤ + + + +XCTRL + +⊤ +:ctrl + + + +TOP->XCTRL + + + + + +INT_TOP + +⊤ +:int + + + +TOP->INT_TOP + + + + + +PTR + + + +*ptr + + +struct + + + +TOP->PTR + + + + + +STRUCT_TOP + +⊤ +:struct + + + +TOP->STRUCT_TOP + + + + + +MEM_TOP + +⊤ +:mem + + + +TOP->MEM_TOP + + + + + +TUPLE_TOP + +⊤ +:tuple + + + +TOP->TUPLE_TOP + + + + + +BOT + +⊥ + + + +CTRL + +⊥ +:ctrl + + + +XCTRL->CTRL + + + + + +CTRL->BOT + + + + + +INT_MINDOTS + +... + + + +INT_TOP->INT_MINDOTS + + + + + +INT_ZERO + +0 + + + +INT_TOP->INT_ZERO + + + + + +INT_MAXDOTS + +... + + + +INT_TOP->INT_MAXDOTS + + + + + +INT_BOT + +⊥ +:int + + + +INT_BOT->BOT + + + + + +INT_MINDOTS->INT_BOT + + + + + + +INT_ZERO->INT_BOT + + + + + + +INT_MAXDOTS->INT_BOT + + + + + +PTR0 + + + +*ptr? + + +struct + + + +PTR->PTR0 + + + + + +PTR0->BOT + + + + + +STRUCT_S1 + + + +field + + + + + + + + +int/ptr + + + +STRUCT_TOP->STRUCT_S1 + + + + + +STRUCT_S2 + + + +field + + + + + + + + +int/ptr + + +field + + + + + + + + +int/ptr + + + +STRUCT_TOP->STRUCT_S2 + + + + + +STRUCT_SN + + + +field + + + + + + + + +int/ptr + + +...   + + + + + + + + +int/ptr + + +field + + + + + + + + +int/ptr + + + +STRUCT_TOP->STRUCT_SN + + + + + +STRUCT_BOT + +⊥ +:struct + + + +STRUCT_BOT->BOT + + + + + +STRUCT_S1->STRUCT_BOT + + + + + + +STRUCT_S2->STRUCT_BOT + + + + + + +STRUCT_SN->STRUCT_BOT + + + + + +MEM_ALIAS1 + +#1 + + + +MEM_TOP->MEM_ALIAS1 + + + + + +MEM_ALIASDOTS + +... + + + +MEM_TOP->MEM_ALIASDOTS + + + + + +MEM_ALIASN + +#n + + + +MEM_TOP->MEM_ALIASN + + + + + +MEM_BOT + +⊥ +:mem + + + +MEM_BOT->BOT + + + + + +MEM_ALIAS1->MEM_BOT + + + + + + +MEM_ALIASDOTS->MEM_BOT + + + + + + +MEM_ALIASN->MEM_BOT + + + + + +TUPLE1 + + + +Tuple + + +type + + + +TUPLE_TOP->TUPLE1 + + + + + +TUPLE2 + + + +Tuple + + +type + + +type + + + +TUPLE_TOP->TUPLE2 + + + + + +TUPLEN + + + +Tuple + + +type + + +... + + +type + + + +TUPLE_TOP->TUPLEN + + + + + +TUPLE_BOT + +⊥ +:tuple + + + +TUPLE_BOT->BOT + + + + + +TUPLE1->TUPLE_BOT + + + + + +TUPLE2->TUPLE_BOT + + + + + +TUPLEN->TUPLE_BOT + + + + + 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 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/GraphVisualizer.java b/src/main/java/com/seaofnodes/simple/GraphVisualizer.java index b4d64d4c..cfe6d5cc 100644 --- a/src/main/java/com/seaofnodes/simple/GraphVisualizer.java +++ b/src/main/java/com/seaofnodes/simple/GraphVisualizer.java @@ -27,7 +27,7 @@ public class GraphVisualizer { // nodes in the graph. Collection all = findAll(stop, scope); StringBuilder sb = new StringBuilder(); - sb.append("digraph chapter09 {\n"); + sb.append("digraph chapter10 {\n"); sb.append("/*\n"); sb.append(stop._src); sb.append("\n*/\n"); @@ -39,8 +39,10 @@ public class GraphVisualizer { // still making the subgraphs DOT gets confused. sb.append("\trankdir=BT;\n"); // Force Nodes before Scopes + // CNC - turned off Apr/8/2024, gives more flex in the layout and + // removes some of the more ludicrous layout choices. // Preserve node input order - sb.append("\tordering=\"in\";\n"); + //sb.append("\tordering=\"in\";\n"); // Merge multiple edges hitting the same node. Makes common shared // nodes much prettier to look at. @@ -195,9 +197,14 @@ public class GraphVisualizer { } else sb.append(def.uniqueName()); // Number edges, so we can see how they track sb.append("[taillabel=").append(i); + // The edge from New to ctrl is just for anchoring the New + if ( n instanceof NewNode ) + sb.append(" color=green"); // control edges are colored red - if( def.isCFG() ) + else if( def.isCFG() ) sb.append(" color=red"); + else if( def.isMem() ) + sb.append(" color=blue"); // Backedges do not add a ranking constraint if( i==2 && (n instanceof PhiNode || n instanceof LoopNode) ) sb.append(" constraint=false"); diff --git a/src/main/java/com/seaofnodes/simple/IRPrinter.java b/src/main/java/com/seaofnodes/simple/IRPrinter.java index c06ac9d0..f08f3208 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._inputs==null ) { + 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 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 6dafd95f..1cee1ccb 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; @@ -90,6 +88,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( show ) diff --git a/src/main/java/com/seaofnodes/simple/Parser.java b/src/main/java/com/seaofnodes/simple/Parser.java index aabdf63a..fc84c9d9 100644 --- a/src/main/java/com/seaofnodes/simple/Parser.java +++ b/src/main/java/com/seaofnodes/simple/Parser.java @@ -45,14 +45,17 @@ public class Parser { /** * List of keywords disallowed as identifiers */ - private final HashSet KEYWORDS = new HashSet<>(){{ + private static final HashSet 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"); }}; @@ -67,9 +70,13 @@ public class Parser { ScopeNode _continueScope; ScopeNode _breakScope; + // Mapping from a Struct name to a Struct. + public static Map OBJS = new HashMap<>(); + public Parser(String source, TypeInteger arg) { Node.reset(); IterPeeps.reset(); + OBJS.clear(); _lexer = new Lexer(source); _scope = new ScopeNode(); _continueScope = _breakScope = null; @@ -98,8 +105,8 @@ 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, 1, ScopeNode.ARG0).peephole()); parseBlock(); _scope.pop(); _xScopes.pop(); @@ -137,18 +144,60 @@ 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("struct") ) return parseStruct(); else if (matchx("#showGraph")) return require(showGraph(),";"); - else if (matchx(";")) return null; // Empty statement + 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. + *
+     *     int IDENTIFIER ;
+     * 
+ */ + 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 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 + START.addMemProj(ts, _scope); // Insert memory edges + return parseStatement(); + } + /** * Parses a while statement * @@ -241,7 +290,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 @@ -266,7 +315,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 @@ -280,6 +329,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; @@ -287,9 +337,11 @@ public class Parser { _scope = fScope; // Restore scope, then parse else block if any ctrl(ifF.unkeep()); // Ctrl token is now set to ifFalse projection 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"); @@ -313,7 +365,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 * *
-     *     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
      * 
* @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 * *
-     *     'int' name = expression ';'
+     *     type name = expression ';'
      * 
- * @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. * *
-     *     unaryExpr : ('-') unaryExpr | primaryExpr
+     *     unaryExpr : ('-') | '!') unaryExpr | postfixExpr | primaryExpr
      * 
* @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: * *
-     *     primaryExpr : integerLiteral | Identifier | true | false | '(' expression ')'
+     *     primaryExpr : integerLiteral | Identifier | true | false | null | new Identifier | '(' expression ')'
      * 
* @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); @@ -477,6 +583,61 @@ public class Parser { throw error("Undefined name '" + name + "'"); } + /** + * 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(); + int alias = START._aliasStarts.get(obj._name); + for( Field field : obj._fields ) { + memAlias(alias, new StoreNode(field._fname, alias, memAlias(alias), n, initValue).peephole()); + alias++; + } + return n.unkeep(); + } + + // We set up memory aliases by inserting special vars in the scope these + // variables are prefixed by $ so they cannot be referenced in Simple code. + // Using vars has the benefit that all the existing machinery of scoping + // and phis work as expected + private Node memAlias(int alias ) { return _scope.lookup(memName(alias) ); } + private Node memAlias(int alias, Node st) { return _scope.update(memName(alias), st); } + public static String memName(int alias) { return ("$"+alias).intern(); } + + /** + * Parse postfix expression. For now this is just a field + * expression, but in future could be array index too. + * + *
+     *     expr ('.' IDENTIFIER)* [ = expr ]
+     * 
+ */ + 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() + "'"); + int alias = START._aliasStarts.get(ptr._obj._name)+idx; + + if( match("=") ) { + // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment + if( peek('=') ) _lexer._position--; + else { + Node val = parseExpression(); + memAlias(alias, new StoreNode(name, alias, memAlias(alias), 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, alias, declaredType, memAlias(alias), expr).peephole()); + } + /** * Parse integer literal * @@ -497,6 +658,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 +667,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 require(N n, String syntax) { + private Parser require(String syntax) { require(null, syntax); return this; } + private N require(N n, String syntax) { if (match(syntax)) return n; throw errorSyntax(syntax); } @@ -594,6 +758,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 +772,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 int find( E[] ary, E x ) { + for( int i=0; i>>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 4e30bb55..2cc0b465 100644 --- a/src/main/java/com/seaofnodes/simple/node/AddNode.java +++ b/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -132,7 +132,7 @@ public class AddNode extends Node { for( int i=1; i 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 ((x * (phi cons)) * con) ? // Do we have ((x * (phi cons)) * (phi cons)) ? // Push constant up through the phi: x * (phi con0*con0 con1*con1...) - 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 9e45198c..ca42c2dc 100644 --- a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java @@ -51,8 +51,4 @@ public class ConstantNode extends Node { @Override int hash() { return _con.hashCode(); } - - // Dead control is its own idom root - Node idom() { return null; } - } 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..53ae1096 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -0,0 +1,78 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.Field; + +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 memory alias node - this is updated after a Store + * @param memPtr The ptr to the struct from where we load a field + */ + public LoadNode(String name, int alias, Type glb, Node memSlice, Node memPtr) { + super(name, alias, memSlice, memPtr, null); + _declaredType = glb; + } + + @Override + public String label() { return "Load"; } + + @Override + public String glabel() { return "."+_name; } + + @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() ) { // Must check same object + assert Utils.eq(_name,st._name); // Equiv class aliasing is perfect + 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,_alias,_declaredType,memphi.in(1),ptr()).peephole(); + Node ld2 = new LoadNode(_name,_alias,_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(); + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/src/main/java/com/seaofnodes/simple/node/LoopNode.java index c192bc20..ea1489c4 100644 --- a/src/main/java/com/seaofnodes/simple/node/LoopNode.java +++ b/src/main/java/com/seaofnodes/simple/node/LoopNode.java @@ -25,5 +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(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..08e08bbf --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Field; +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 final int _alias; + + public MemOpNode(String name, int alias, Node memSlice, Node memPtr, Node value) { + super(null, memSlice, memPtr, value); + _name = name; + _alias = alias; + } + + public Node mem() { return in(1); } + public Node ptr() { return in(2); } + + @Override + boolean eq(Node n) { + MemOpNode mem = (MemOpNode)n; // Invariant + return _alias==mem._alias; // When comparing types error to use "equals"; always use "==" + } + + @Override + int hash() { return _alias; } + + @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 dd092c9e..9b687613 100644 --- a/src/main/java/com/seaofnodes/simple/node/MinusNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MinusNode.java @@ -26,5 +26,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..352689b5 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/NewNode.java @@ -0,0 +1,40 @@ +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 + public String glabel() { return "new " + _ptr._obj._name; } + + @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 5eb9c076..bb698860 100644 --- a/src/main/java/com/seaofnodes/simple/node/Node.java +++ b/src/main/java/com/seaofnodes/simple/node/Node.java @@ -50,15 +50,6 @@ public abstract class Node { public Type _type; - /** - * 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. - *

- * See {@link ...} - */ - int _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. @@ -81,7 +72,11 @@ public abstract class Node { public abstract String label(); // Unique label for graph visualization, e.g. "Add12" or "Region30" or "EQ99" - public String uniqueName() { return label() + _nid; } + public String uniqueName() { + // Get rid of $ as graphviz doesn't like it + String label = label().replaceAll("\\$", ""); + return label + _nid; + } // Graphical label, e.g. "+" or "Region" or "==" public String glabel() { return label(); } @@ -118,31 +113,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( _inputs==null ) { - 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(); idef into a Node. Keeps the edges correct, by removing * the corresponding use->def edge. This may make the original @@ -197,6 +169,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; } @@ -283,7 +256,12 @@ public abstract class Node { // Add bogus null use to keep node alive public N keep() { return addUse(null); } // Remove bogus null. - public N unkeep() { delUse(null); return (N)this; } + public 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 @@ -345,7 +323,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 @@ -380,7 +358,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(). @@ -478,9 +456,9 @@ public abstract class Node { ArrayList _deps; /** - * 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 @@ -567,14 +545,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 + * See {@link ...} + */ + public int _idepth; // IDOM depth approx; Zero is unset; non-zero is cached legit + final int _idepth(int idx) { return _idepth==0 ? (_idepth=in(idx).idepth()+1) : _idepth; } + int idepth() { return _idepth(0); } + + // 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 7a520fec..05e44eb0 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 { @@ -20,9 +19,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 TypeInteger.TOP.meet(in(1)._type); + 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 ? Type.TOP : Type.BOTTOM; + } } @Override diff --git a/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/src/main/java/com/seaofnodes/simple/node/PhiNode.java index ec4c6ad3..dd67bd06 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; } @@ -31,17 +37,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; } @@ -71,11 +81,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 c949c2b2..42501118 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,17 +52,25 @@ public class RegionNode extends Node { while( nouts != nOuts() ) { nouts = nOuts(); for( int i=0; i= 0 ) lhs = lhs.idom(); if( comp <= 0 ) rhs = rhs.idom(); } - if( lhs==null ) return null; - _idepth = lhs._idepth+1; - if( !IterPeeps.midAssert() ) _idom=lhs; return lhs; } diff --git a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java index 922a3649..92bda5bf 100644 --- a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -15,8 +15,15 @@ import java.util.BitSet; */ public class ReturnNode extends Node { - public ReturnNode(Node ctrl, Node data) { + public ReturnNode(Node ctrl, Node data, ScopeNode scope) { super(ctrl, data); + // We lookup memory slices by the naming convention that they start with $ + // We could also use implicit knowledge that all memory projects are at offset >= 2 + String[] names = scope.reverseNames(); + for (String name: names) { + if (!name.equals("$ctrl") && name.startsWith("$")) + addDef(scope.lookup(name)); + } } public Node ctrl() { return in(0); } @@ -27,7 +34,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..843ce733 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> _scopes; + /** + * Tracks declared types for every name + */ + public Stack> _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 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. - * + *

* 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 syms : _scopes ) dup._scopes.push(new HashMap<>(syms)); + for( HashMap 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 _aliasStarts; + public StartNode(Type[] args) { super(); _args = TypeTuple.make(args); _type = _args; + _aliasStarts = new HashMap<>(); + } + + /** + * Creates a projection for each of the struct's fields, using the field alias + * as the key. + */ + public void addMemProj( TypeStruct ts, ScopeNode scope) { + int len = _args._types.length; + _aliasStarts.put(ts._name,len); + + // resize the tuple's type array to include all fields of the struct + int max = len + ts._fields.length; + Type[] args = Arrays.copyOf(_args._types, max); + + // The new members of the tuple get a mem type with an alias + for( int alias = len; alias < max; alias++ ) + args[alias] = TypeMem.make(alias); + _type = _args = TypeTuple.make(args); + // For each of the fields we now add a mem projection. Note that the + // alias matches the slot of the field in the tuple + for( int alias = len; alias < max; alias++ ) { + String name = Parser.memName(alias); + Node n = new ProjNode(this, alias, name).peephole(); + scope.define(name, args[alias], n); + } } @Override @@ -40,7 +73,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 602feffc..3b5a7ce3 100644 --- a/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -51,10 +51,17 @@ 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,false); } - public StopNode iterate(boolean show) { return IterPeeps.iterate(this,show ); } + public StopNode iterate( ) { return IterPeeps.iterate(this,false).typeCheck(); } + public StopNode iterate(boolean show) { return IterPeeps.iterate(this,show ).typeCheck(); } + StopNode typeCheck() { + String err = walk( Node::err ); + if( err != null ) throw new RuntimeException(err); + 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..8d264601 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -0,0 +1,70 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +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 memory alias 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, int alias, Node memSlice, Node memPtr, Node value) { + super(name, alias, memSlice, memPtr, value); + } + + @Override + public String label() { return "Store"; } + @Override + public String glabel() { return "."+_name+" ="; } + @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.make(_alias); } + + @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() && // 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) ) { + assert Utils.eq(_name,st._name); // Equiv class aliasing is perfect + 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/node/SubNode.java b/src/main/java/com/seaofnodes/simple/node/SubNode.java index f661be14..b87115cd 100644 --- a/src/main/java/com/seaofnodes/simple/node/SubNode.java +++ b/src/main/java/com/seaofnodes/simple/node/SubNode.java @@ -35,6 +35,14 @@ public class SubNode extends Node { @Override public Node idealize() { + // x - (-y) is x+y + if( in(2) instanceof MinusNode minus ) + return new AddNode(in(1),minus.in(1)); + + // (-x) - y is -(x+y) + if( in(1) instanceof MinusNode minus ) + return new MinusNode(new AddNode(minus.in(1),in(2)).peephole()); + return null; } 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..56626482 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/type/Field.java @@ -0,0 +1,60 @@ +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; + } + // Make with existing alias + 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 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 d0265e83..aa0ba1ea 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; // All memory (alias 0) or A slice of memory - with specific alias + 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 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 intern() { + public 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 ba045603..f2a93a3a 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 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; } @@ -64,6 +86,11 @@ public class TypeInteger extends Type { return _con==0 ? BOT : TOP; } + @Override + public Type glb() { return BOT; } + + @Override public TypeInteger makeInit() { return ZERO; } + @Override int hash() { return (int)(_con ^ (_is_con ? 0 : 0x4000)); } @Override 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..198d49ee --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/type/TypeMem.java @@ -0,0 +1,64 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; +import java.lang.Long; +import java.util.BitSet; +import java.util.ArrayList; + + +/** + * Represents a slice of memory corresponding to a set of aliases + */ +public class TypeMem extends Type { + + // Which slice of memory? + // 0 means TOP, no slice. + // -1 means BOT, all memory. + // N means slice#N. + private final int _alias; + + private TypeMem(int alias) { super(TMEM); _alias = alias; } + + public static TypeMem make(int alias) { return new TypeMem(alias).intern(); } + public static final TypeMem TOP = make( 0); + public static final TypeMem BOT = make(-1); + + + public static void gather(ArrayList ts) { ts.add(make(1)); ts.add(BOT); } + + @Override + TypeMem xmeet(Type t) { + TypeMem that = (TypeMem) t; // Invariant: TypeMem and unequal + return _alias==0 ? that : (that._alias==0 ? this : BOT); + } + + @Override + public Type dual() { + if( _alias== 0 ) return BOT; + if( _alias==-1 ) return TOP; + return this; + } + + @Override + public Type glb() { return TypeMem.BOT; } + + @Override + int hash() { return 9876543 + _alias; } + + @Override + boolean eq(Type t) { + TypeMem that = (TypeMem) t; // Invariant + return _alias == that._alias; + } + + @Override + public StringBuilder _print(StringBuilder sb) { + return sb.append("MEM#").append( switch(_alias) { + case 0 -> "TOP"; + case -1 -> "BOT"; + default -> ""+_alias; + }); + } + + @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..65352402 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java @@ -0,0 +1,78 @@ +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 TypeMemPtr BOT = make(TypeStruct.BOT,true); + public static TypeMemPtr TOP = BOT.dual(); + public static TypeMemPtr NULLPTR = make(TypeStruct.TOP,true); + public static TypeMemPtr VOIDPTR = NULLPTR.dual(); // A bottom mix of not-null ptrs, like C's void* but not null + public static TypeMemPtr TEST= make(TypeStruct.TEST,false); + public static void gather(ArrayList 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== 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== 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..29102675 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -0,0 +1,121 @@ +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 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 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) { + 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() { 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 e82ea00b..d57f6c98 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 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]; @@ -24,6 +29,14 @@ public class TypeTuple extends Type { return make(ts); } + @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("["); @@ -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 a243beb1..c76df903 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter03Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter03Test.java @@ -40,7 +40,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..35e54fa9 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(Region18,(arg+2),(arg-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(Region21,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(Region32,(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(Region36,Phi(Region22,2,3),Phi(Region34,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(Region13,1,0))+Phi(Region28,2,0));", stop.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..097c2617 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter06Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter06Test.java @@ -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(Region37,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(Region30,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(Region41,3,Phi(Region39,4,5));", stop.toString()); } @Test @@ -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(Region36,Phi(Region23,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 adf10471..de9d16f2 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter07Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter07Test.java @@ -37,7 +37,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(Region24,1,Phi(Loop13,1,(Phi_a+1)));", stop.toString()); } @Test @@ -74,7 +74,7 @@ return b; """); Node._disablePeephole = true; StopNode stop = parser.parse(); - assertEquals("return Phi(Loop8,2,Phi(Region26,Phi_b,4));", stop.toString()); + assertEquals("return Phi(Loop8,2,Phi(Region27,Phi_b,4));", stop.toString()); assertTrue(stop.ret().ctrl() instanceof ProjNode); Node._disablePeephole = false; } @@ -94,7 +94,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(Loop8,2,(Phi(Region27,Phi_b,4)+1));", 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..6688d598 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; @@ -25,8 +26,8 @@ return arg; StopNode stop = parser.parse().iterate(); assertEquals("return Phi(Region36,Phi(Region25,Phi(Loop6,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)); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/src/test/java/com/seaofnodes/simple/Chapter09Test.java index 5567299a..048a58c7 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 @@ -70,7 +71,7 @@ return arg; """); StopNode stop = parser.parse().iterate(); assertEquals("return Phi(Loop7,arg,(Phi_arg+2));", stop.toString()); - Assert.assertEquals(11, GraphEvaluator.evaluate(stop, 1)); + Assert.assertEquals(11L, Evaluator.evaluate(stop, 1)); } @Test @@ -87,7 +88,7 @@ return arg; """); StopNode stop = parser.parse().iterate(); assertEquals("return Phi(Loop8,arg,(Phi_arg+4));", stop.toString()); - Assert.assertEquals(13, GraphEvaluator.evaluate(stop, 1)); + Assert.assertEquals(13L, Evaluator.evaluate(stop, 1)); } @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..f2e7ca2c --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -0,0 +1,404 @@ +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 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(); + assertEquals("return Phi(Loop14,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum));", stop.toString()); + } + + + + + @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 {"); + for(int i=0; i= 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 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] = parameter; + for(int i=2;i 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 + *
+ * This scheduler is used for tests and is not the final scheduler. + *
+ *
+ * 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. + *
+ * 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 if 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. + *
+ *
+ * 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 a.depth < b.depth 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 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 data = new IdentityHashMap<>(); + /** + * List of nodes which can be placed since all users are placed. + */ + private final Stack 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 + * Optional.empty() for null or dead nodes. + */ + Optional 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 a and b + * @param a Block a + * @param b Block b + * @return The dominator of a and b + */ + 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{ + // 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; id.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 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; iupdate(d(in), d.block)); + } + } + } + + /** + * Build the control flow graph. + * @param start The start node. + */ + private void doBuildCTF(StartNode start) { + var queue = new Stack(); + var phiQueue = new Stack(); + 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(); + for(int i=1; iprev.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 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(); + var dataQueue = new Stack(); + var mem = new Stack(); + 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; id.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(); + var queue = new Stack(); + 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(); + 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 936fb41e..51e39cd0 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 reproducer) { + private void recordException(Throwable e, String script, Consumer 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;irunCheck(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); } diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java index f13a51e1..18ef069a 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 getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { @@ -74,10 +74,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); } @@ -96,7 +98,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 {} + /** * 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 variables = new ArrayList<>(); + private final ArrayList variables = new ArrayList<>(); + /** + * All defined structs. + */ + private final ArrayList 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; i2) 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 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. @@ -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 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 { 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) { @@ -318,13 +528,40 @@ public class ScriptGenerator { return stop; } + /** + * 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(); @@ -332,16 +569,62 @@ public class ScriptGenerator { return 0; } + /** + * Generate a counted loop of the form + * + * { + * int varname = start_expr; + * while (varname < end_expr) loop_body; + * } + * + * @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); } } -- cgit v1.2.3