From e1bba17ff54400a34ede0eaacad863420d021119 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Tue, 29 Sep 2026 09:44:41 -0700 Subject: Split ch10 Basic memory introduction in 10a, then precise aliases in 10b. --- .github/workflows/linearize.sh | 2 +- .gitignore | 5 + Makefile | 2 +- README.md | 26 +- TypeAnalysis.md | 6 +- chapter02/README.md | 4 +- chapter03/README.md | 4 +- chapter10/Makefile | 105 --- chapter10/README.md | 359 --------- chapter10/docs/10-grammar.md | 125 --- chapter10/docs/Makefile | 12 - chapter10/docs/example1.gv | 78 -- chapter10/docs/example1.svg | 294 ------- chapter10/docs/example2a.gv | 65 -- chapter10/docs/example2a.svg | 249 ------ chapter10/docs/example2b.gv | 56 -- chapter10/docs/example2b.svg | 189 ----- chapter10/docs/example2c.gv | 53 -- chapter10/docs/example2c.svg | 168 ---- chapter10/docs/lattice.drawio | 379 --------- chapter10/docs/lattice.gv | 161 ---- chapter10/docs/lattice.svg | 516 ------------- chapter10/pom.xml | 17 - .../main/java/com/seaofnodes/simple/IRPrinter.java | 188 ----- .../main/java/com/seaofnodes/simple/IterPeeps.java | 208 ----- .../main/java/com/seaofnodes/simple/Parser.java | 834 -------------------- .../src/main/java/com/seaofnodes/simple/Utils.java | 65 -- .../java/com/seaofnodes/simple/node/AddNode.java | 176 ----- .../java/com/seaofnodes/simple/node/BoolNode.java | 76 -- .../java/com/seaofnodes/simple/node/CastNode.java | 38 - .../com/seaofnodes/simple/node/ConstantNode.java | 54 -- .../java/com/seaofnodes/simple/node/DivNode.java | 45 -- .../java/com/seaofnodes/simple/node/IfNode.java | 62 -- .../java/com/seaofnodes/simple/node/LoadNode.java | 75 -- .../java/com/seaofnodes/simple/node/LoopNode.java | 31 - .../java/com/seaofnodes/simple/node/MemOpNode.java | 40 - .../java/com/seaofnodes/simple/node/MinusNode.java | 34 - .../java/com/seaofnodes/simple/node/MulNode.java | 59 -- .../java/com/seaofnodes/simple/node/MultiNode.java | 14 - .../java/com/seaofnodes/simple/node/NewNode.java | 37 - .../main/java/com/seaofnodes/simple/node/Node.java | 633 ---------------- .../java/com/seaofnodes/simple/node/NotNode.java | 42 - .../java/com/seaofnodes/simple/node/PhiNode.java | 157 ---- .../java/com/seaofnodes/simple/node/ProjNode.java | 59 -- .../com/seaofnodes/simple/node/RegionNode.java | 118 --- .../com/seaofnodes/simple/node/ReturnNode.java | 55 -- .../java/com/seaofnodes/simple/node/ScopeNode.java | 304 -------- .../java/com/seaofnodes/simple/node/StartNode.java | 78 -- .../java/com/seaofnodes/simple/node/StopNode.java | 74 -- .../java/com/seaofnodes/simple/node/StoreNode.java | 68 -- .../java/com/seaofnodes/simple/node/SubNode.java | 63 -- .../simple/print/SimpleGraphAdapter.java | 62 -- .../simple/print/SimpleGraphObserver.java | 45 -- .../java/com/seaofnodes/simple/type/Field.java | 60 -- .../main/java/com/seaofnodes/simple/type/Type.java | 187 ----- .../com/seaofnodes/simple/type/TypeInteger.java | 101 --- .../java/com/seaofnodes/simple/type/TypeMem.java | 66 -- .../com/seaofnodes/simple/type/TypeMemPtr.java | 82 -- .../com/seaofnodes/simple/type/TypeStruct.java | 127 ---- .../java/com/seaofnodes/simple/type/TypeTuple.java | 88 --- .../java/com/seaofnodes/simple/Chapter01Test.java | 100 --- .../java/com/seaofnodes/simple/Chapter02Test.java | 60 -- .../java/com/seaofnodes/simple/Chapter03Test.java | 69 -- .../java/com/seaofnodes/simple/Chapter04Test.java | 162 ---- .../java/com/seaofnodes/simple/Chapter05Test.java | 213 ------ .../java/com/seaofnodes/simple/Chapter06Test.java | 294 ------- .../java/com/seaofnodes/simple/Chapter07Test.java | 222 ------ .../java/com/seaofnodes/simple/Chapter08Test.java | 209 ----- .../java/com/seaofnodes/simple/Chapter09Test.java | 301 -------- .../java/com/seaofnodes/simple/Chapter10Test.java | 461 ----------- .../java/com/seaofnodes/simple/FuzzerWrap.java | 50 -- .../java/com/seaofnodes/simple/SchedulerTest.java | 73 -- .../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 | 199 ----- .../com/seaofnodes/simple/fuzzer/FuzzerUtils.java | 142 ---- .../java/com/seaofnodes/simple/fuzzer/Reducer.java | 176 ----- .../seaofnodes/simple/fuzzer/ScriptGenerator.java | 792 ------------------- chapter10a/Makefile | 7 + chapter10a/README.md | 273 +++++++ chapter10a/chapter10a.iml | 27 + chapter10a/docs/10-grammar.md | 125 +++ chapter10a/docs/Makefile | 14 + chapter10a/docs/example1.gv | 10 + chapter10a/docs/example1.svg | 73 ++ chapter10a/docs/example2a.gv | 10 + chapter10a/docs/example2a.svg | 85 +++ chapter10a/docs/example2b.gv | 8 + chapter10a/docs/example2b.svg | 73 ++ chapter10a/docs/example2c.gv | 9 + chapter10a/docs/example2c.svg | 50 ++ chapter10a/docs/lattice.gv | 151 ++++ chapter10a/docs/lattice.svg | 466 ++++++++++++ chapter10a/pom.xml | 17 + .../main/java/com/seaofnodes/simple/IRPrinter.java | 188 +++++ .../main/java/com/seaofnodes/simple/IterPeeps.java | 208 +++++ .../main/java/com/seaofnodes/simple/Parser.java | 827 ++++++++++++++++++++ .../src/main/java/com/seaofnodes/simple/Utils.java | 65 ++ .../java/com/seaofnodes/simple/node/AddNode.java | 176 +++++ .../java/com/seaofnodes/simple/node/BoolNode.java | 76 ++ .../java/com/seaofnodes/simple/node/CastNode.java | 38 + .../com/seaofnodes/simple/node/ConstantNode.java | 54 ++ .../java/com/seaofnodes/simple/node/DivNode.java | 45 ++ .../java/com/seaofnodes/simple/node/IfNode.java | 62 ++ .../java/com/seaofnodes/simple/node/LoadNode.java | 72 ++ .../java/com/seaofnodes/simple/node/LoopNode.java | 31 + .../java/com/seaofnodes/simple/node/MemOpNode.java | 37 + .../java/com/seaofnodes/simple/node/MinusNode.java | 34 + .../java/com/seaofnodes/simple/node/MulNode.java | 59 ++ .../java/com/seaofnodes/simple/node/MultiNode.java | 14 + .../java/com/seaofnodes/simple/node/NewNode.java | 37 + .../main/java/com/seaofnodes/simple/node/Node.java | 633 ++++++++++++++++ .../java/com/seaofnodes/simple/node/NotNode.java | 42 + .../java/com/seaofnodes/simple/node/PhiNode.java | 157 ++++ .../java/com/seaofnodes/simple/node/ProjNode.java | 59 ++ .../com/seaofnodes/simple/node/RegionNode.java | 118 +++ .../com/seaofnodes/simple/node/ReturnNode.java | 40 + .../java/com/seaofnodes/simple/node/ScopeNode.java | 304 ++++++++ .../java/com/seaofnodes/simple/node/StartNode.java | 35 + .../java/com/seaofnodes/simple/node/StopNode.java | 74 ++ .../java/com/seaofnodes/simple/node/StoreNode.java | 66 ++ .../java/com/seaofnodes/simple/node/SubNode.java | 63 ++ .../simple/print/SimpleGraphAdapter.java | 62 ++ .../simple/print/SimpleGraphObserver.java | 45 ++ .../java/com/seaofnodes/simple/type/Field.java | 59 ++ .../main/java/com/seaofnodes/simple/type/Type.java | 187 +++++ .../com/seaofnodes/simple/type/TypeInteger.java | 101 +++ .../java/com/seaofnodes/simple/type/TypeMem.java | 20 + .../com/seaofnodes/simple/type/TypeMemPtr.java | 82 ++ .../com/seaofnodes/simple/type/TypeStruct.java | 127 ++++ .../java/com/seaofnodes/simple/type/TypeTuple.java | 88 +++ .../java/com/seaofnodes/simple/Chapter01Test.java | 100 +++ .../java/com/seaofnodes/simple/Chapter02Test.java | 60 ++ .../java/com/seaofnodes/simple/Chapter03Test.java | 69 ++ .../java/com/seaofnodes/simple/Chapter04Test.java | 162 ++++ .../java/com/seaofnodes/simple/Chapter05Test.java | 213 ++++++ .../java/com/seaofnodes/simple/Chapter06Test.java | 294 +++++++ .../java/com/seaofnodes/simple/Chapter07Test.java | 222 ++++++ .../java/com/seaofnodes/simple/Chapter08Test.java | 209 +++++ .../java/com/seaofnodes/simple/Chapter09Test.java | 301 ++++++++ .../java/com/seaofnodes/simple/Chapter10Test.java | 522 +++++++++++++ .../java/com/seaofnodes/simple/FuzzerWrap.java | 50 ++ .../java/com/seaofnodes/simple/SchedulerTest.java | 73 ++ .../test/java/com/seaofnodes/simple/TypeTest.java | 132 ++++ .../com/seaofnodes/simple/evaluator/Evaluator.java | 209 +++++ .../com/seaofnodes/simple/evaluator/Scheduler.java | 592 +++++++++++++++ .../java/com/seaofnodes/simple/fuzzer/Fuzzer.java | 199 +++++ .../com/seaofnodes/simple/fuzzer/FuzzerUtils.java | 142 ++++ .../java/com/seaofnodes/simple/fuzzer/Reducer.java | 176 +++++ .../seaofnodes/simple/fuzzer/ScriptGenerator.java | 792 +++++++++++++++++++ chapter10b/Makefile | 7 + chapter10b/README.md | 175 +++++ chapter10b/chapter10b.iml | 27 + chapter10b/docs/10-grammar.md | 125 +++ chapter10b/docs/Makefile | 14 + chapter10b/docs/example1.gv | 11 + chapter10b/docs/example1.svg | 91 +++ chapter10b/docs/example2a.gv | 10 + chapter10b/docs/example2a.svg | 82 ++ chapter10b/docs/example2b.gv | 10 + chapter10b/docs/example2b.svg | 101 +++ chapter10b/docs/example2c.gv | 10 + chapter10b/docs/example2c.svg | 56 ++ chapter10b/docs/lattice.gv | 161 ++++ chapter10b/docs/lattice.svg | 516 +++++++++++++ chapter10b/pom.xml | 17 + .../main/java/com/seaofnodes/simple/IRPrinter.java | 188 +++++ .../main/java/com/seaofnodes/simple/IterPeeps.java | 208 +++++ .../main/java/com/seaofnodes/simple/Parser.java | 843 +++++++++++++++++++++ .../src/main/java/com/seaofnodes/simple/Utils.java | 65 ++ .../java/com/seaofnodes/simple/node/AddNode.java | 176 +++++ .../java/com/seaofnodes/simple/node/BoolNode.java | 76 ++ .../com/seaofnodes/simple/node/BulkMemPhiNode.java | 237 ++++++ .../java/com/seaofnodes/simple/node/CastNode.java | 38 + .../com/seaofnodes/simple/node/ConstantNode.java | 54 ++ .../java/com/seaofnodes/simple/node/DivNode.java | 45 ++ .../java/com/seaofnodes/simple/node/IfNode.java | 62 ++ .../java/com/seaofnodes/simple/node/LoadNode.java | 81 ++ .../java/com/seaofnodes/simple/node/LoopNode.java | 31 + .../com/seaofnodes/simple/node/MemMergeNode.java | 60 ++ .../java/com/seaofnodes/simple/node/MemOpNode.java | 40 + .../com/seaofnodes/simple/node/MemPhiNode.java | 36 + .../java/com/seaofnodes/simple/node/MinusNode.java | 34 + .../java/com/seaofnodes/simple/node/MulNode.java | 59 ++ .../java/com/seaofnodes/simple/node/MultiNode.java | 14 + .../java/com/seaofnodes/simple/node/NewNode.java | 37 + .../main/java/com/seaofnodes/simple/node/Node.java | 641 ++++++++++++++++ .../java/com/seaofnodes/simple/node/NotNode.java | 42 + .../java/com/seaofnodes/simple/node/PhiNode.java | 161 ++++ .../java/com/seaofnodes/simple/node/ProjNode.java | 59 ++ .../com/seaofnodes/simple/node/RegionNode.java | 118 +++ .../com/seaofnodes/simple/node/ReturnNode.java | 40 + .../java/com/seaofnodes/simple/node/ScopeNode.java | 304 ++++++++ .../java/com/seaofnodes/simple/node/StartNode.java | 35 + .../java/com/seaofnodes/simple/node/StopNode.java | 74 ++ .../java/com/seaofnodes/simple/node/StoreNode.java | 74 ++ .../java/com/seaofnodes/simple/node/SubNode.java | 63 ++ .../simple/print/SimpleGraphAdapter.java | 62 ++ .../simple/print/SimpleGraphObserver.java | 45 ++ .../java/com/seaofnodes/simple/type/Field.java | 59 ++ .../main/java/com/seaofnodes/simple/type/Type.java | 187 +++++ .../com/seaofnodes/simple/type/TypeInteger.java | 101 +++ .../java/com/seaofnodes/simple/type/TypeMem.java | 66 ++ .../com/seaofnodes/simple/type/TypeMemPtr.java | 82 ++ .../com/seaofnodes/simple/type/TypeStruct.java | 127 ++++ .../java/com/seaofnodes/simple/type/TypeTuple.java | 88 +++ .../java/com/seaofnodes/simple/Chapter01Test.java | 100 +++ .../java/com/seaofnodes/simple/Chapter02Test.java | 60 ++ .../java/com/seaofnodes/simple/Chapter03Test.java | 69 ++ .../java/com/seaofnodes/simple/Chapter04Test.java | 162 ++++ .../java/com/seaofnodes/simple/Chapter05Test.java | 213 ++++++ .../java/com/seaofnodes/simple/Chapter06Test.java | 294 +++++++ .../java/com/seaofnodes/simple/Chapter07Test.java | 222 ++++++ .../java/com/seaofnodes/simple/Chapter08Test.java | 209 +++++ .../java/com/seaofnodes/simple/Chapter09Test.java | 301 ++++++++ .../java/com/seaofnodes/simple/Chapter10Test.java | 522 +++++++++++++ .../java/com/seaofnodes/simple/Chapter10bTest.java | 64 ++ .../java/com/seaofnodes/simple/FuzzerWrap.java | 50 ++ .../java/com/seaofnodes/simple/SchedulerTest.java | 73 ++ .../test/java/com/seaofnodes/simple/TypeTest.java | 145 ++++ .../com/seaofnodes/simple/evaluator/Evaluator.java | 210 +++++ .../com/seaofnodes/simple/evaluator/Scheduler.java | 599 +++++++++++++++ .../java/com/seaofnodes/simple/fuzzer/Fuzzer.java | 199 +++++ .../com/seaofnodes/simple/fuzzer/FuzzerUtils.java | 142 ++++ .../java/com/seaofnodes/simple/fuzzer/Reducer.java | 176 +++++ .../seaofnodes/simple/fuzzer/ScriptGenerator.java | 792 +++++++++++++++++++ chapter11/README.md | 8 +- docs/chapter-backports.md | 27 +- graph/README.md | 2 +- graph/graph.iml | 15 + pom.xml | 3 +- skills/chapter25-codex-notes.md | 6 +- 233 files changed, 21215 insertions(+), 11822 deletions(-) delete mode 100644 chapter10/Makefile delete mode 100644 chapter10/README.md delete mode 100644 chapter10/docs/10-grammar.md delete mode 100644 chapter10/docs/Makefile delete mode 100644 chapter10/docs/example1.gv delete mode 100644 chapter10/docs/example1.svg delete mode 100644 chapter10/docs/example2a.gv delete mode 100644 chapter10/docs/example2a.svg delete mode 100644 chapter10/docs/example2b.gv delete mode 100644 chapter10/docs/example2b.svg delete mode 100644 chapter10/docs/example2c.gv delete mode 100644 chapter10/docs/example2c.svg delete mode 100644 chapter10/docs/lattice.drawio delete mode 100644 chapter10/docs/lattice.gv delete mode 100644 chapter10/docs/lattice.svg delete mode 100644 chapter10/pom.xml delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/IRPrinter.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/IterPeeps.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/Parser.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/Utils.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/AddNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/BoolNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/CastNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/ConstantNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/DivNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/IfNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/LoadNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/LoopNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/MemOpNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/MinusNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/MulNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/MultiNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/NewNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/Node.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/NotNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/PhiNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/ProjNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/RegionNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/ReturnNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/ScopeNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/StartNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/StopNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/StoreNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/node/SubNode.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/type/Field.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/type/Type.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/type/TypeInteger.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/type/TypeMem.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/type/TypeStruct.java delete mode 100644 chapter10/src/main/java/com/seaofnodes/simple/type/TypeTuple.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter01Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter02Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter03Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter04Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter05Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter06Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter07Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter08Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter09Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/Chapter10Test.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/FuzzerWrap.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/SchedulerTest.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/TypeTest.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java delete mode 100644 chapter10/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java create mode 100644 chapter10a/Makefile create mode 100644 chapter10a/README.md create mode 100644 chapter10a/chapter10a.iml create mode 100644 chapter10a/docs/10-grammar.md create mode 100644 chapter10a/docs/Makefile create mode 100644 chapter10a/docs/example1.gv create mode 100644 chapter10a/docs/example1.svg create mode 100644 chapter10a/docs/example2a.gv create mode 100644 chapter10a/docs/example2a.svg create mode 100644 chapter10a/docs/example2b.gv create mode 100644 chapter10a/docs/example2b.svg create mode 100644 chapter10a/docs/example2c.gv create mode 100644 chapter10a/docs/example2c.svg create mode 100644 chapter10a/docs/lattice.gv create mode 100644 chapter10a/docs/lattice.svg create mode 100644 chapter10a/pom.xml create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/IRPrinter.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/IterPeeps.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/Parser.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/Utils.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/AddNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/BoolNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/CastNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/DivNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/IfNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/LoadNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/LoopNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/MinusNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/MulNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/MultiNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/NewNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/Node.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/NotNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/PhiNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/ProjNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/RegionNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/StartNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/StopNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/StoreNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/node/SubNode.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/type/Field.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/type/Type.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/type/TypeMem.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java create mode 100644 chapter10a/src/main/java/com/seaofnodes/simple/type/TypeTuple.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter01Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter02Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter03Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter04Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter05Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter06Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter07Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter08Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter09Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/Chapter10Test.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/FuzzerWrap.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/SchedulerTest.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/TypeTest.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java create mode 100644 chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java create mode 100644 chapter10b/Makefile create mode 100644 chapter10b/README.md create mode 100644 chapter10b/chapter10b.iml create mode 100644 chapter10b/docs/10-grammar.md create mode 100644 chapter10b/docs/Makefile create mode 100644 chapter10b/docs/example1.gv create mode 100644 chapter10b/docs/example1.svg create mode 100644 chapter10b/docs/example2a.gv create mode 100644 chapter10b/docs/example2a.svg create mode 100644 chapter10b/docs/example2b.gv create mode 100644 chapter10b/docs/example2b.svg create mode 100644 chapter10b/docs/example2c.gv create mode 100644 chapter10b/docs/example2c.svg create mode 100644 chapter10b/docs/lattice.gv create mode 100644 chapter10b/docs/lattice.svg create mode 100644 chapter10b/pom.xml create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/IRPrinter.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/IterPeeps.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/Parser.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/Utils.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/AddNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/BoolNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/CastNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/DivNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/IfNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/LoadNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/LoopNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/MinusNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/MulNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/MultiNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/NewNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/Node.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/NotNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/PhiNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/ProjNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/RegionNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/StartNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/StopNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/StoreNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/node/SubNode.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/type/Field.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/type/Type.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/type/TypeMem.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java create mode 100644 chapter10b/src/main/java/com/seaofnodes/simple/type/TypeTuple.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter01Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter02Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter03Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter04Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter05Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter06Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter07Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter08Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter09Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter10Test.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/Chapter10bTest.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/FuzzerWrap.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/SchedulerTest.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/TypeTest.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java create mode 100644 chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java create mode 100644 graph/graph.iml diff --git a/.github/workflows/linearize.sh b/.github/workflows/linearize.sh index ea3499b5..e8785367 100755 --- a/.github/workflows/linearize.sh +++ b/.github/workflows/linearize.sh @@ -84,7 +84,7 @@ while read -r chapter; do cp "$repo"/{README.md,pom.xml} . mkdir docs # Remove links to chapters. - sed -Ei 's,\[Chapter ([0-9]+)\]\(chapter0?\1/README\.md\),Chapter \1,' README.md + sed -Ei 's,\[Chapter ([0-9]+[a-z]?)\]\(chapter0?\1/README\.md\),Chapter \1,' README.md # Change to a JAR and delete the modules, for a single-project structure. sed -i -e 's,pom,jar,' \ -e '//,/^$/d' pom.xml diff --git a/.gitignore b/.gitignore index f1dd0082..e776c5be 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ TAGS # Libs */lib/*jar + +# Portable IDEA descriptors for the split memory chapters +!chapter10a/chapter10a.iml +!chapter10b/chapter10b.iml +!graph/graph.iml diff --git a/Makefile b/Makefile index 76bc9c99..1e097bcf 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # make tests CHAPTERS="chapter21 chapter22" # make -k tests # continue through failures, but return a failing status .DEFAULT_GOAL := tests -CHAPTERS ?= $(sort $(patsubst %/pom.xml,%,$(wildcard chapter[0-9][0-9]/pom.xml))) +CHAPTERS ?= $(sort $(patsubst %/pom.xml,%,$(wildcard chapter[0-9][0-9]/pom.xml chapter[0-9][0-9][a-z]/pom.xml))) ACTIONS := tests tags release lib CHAPTER_TARGETS := $(foreach action,$(ACTIONS),$(addsuffix /$(action),$(CHAPTERS))) diff --git a/README.md b/README.md index 38a74322..4e6d2f32 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ahead-of-time compilation - but with an eye towards JIT compilation. This repo also is not intended to be a complete language in any sense, and so the backend starts with levering Java: the Evaluator (first appears in Chapter -10) directly slowly interprets the SoN IR. Code-gen first appears in Chapter +10a) directly slowly interprets the SoN IR. Code-gen first appears in Chapter 19. @@ -51,9 +51,10 @@ The Simple language is styled after a subset of C or Java. * [Chapter 7](chapter07/README.md): `while` statement; looping constructs - eager phi approach. * [Chapter 8](chapter08/README.md): Looping constructs continued, lazy phi creation, `break` and `continue` statements. * [Chapter 9](chapter09/README.md): Global Value Numbering. Iterative peepholes to fixpoint. Worklists. -* [Chapter 10](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 10a](chapter10a/README.md): User defined structs, pointers and null + analysis. One memory value in SSA. Loads, stores, and an executable evaluator. +* [Chapter 10b](chapter10b/README.md): Equivalence class aliasing. Lazy memory + partitioning with `MemMerge`, `MemPhi`, and `BulkMemPhi`. * [Chapter 11](chapter11/README.md): Global Code Motion - Scheduling. * [Chapter 12](chapter12/README.md): Float type. * [Chapter 13](chapter13/README.md): Nested references in Structs. @@ -73,7 +74,7 @@ The Simple language is styled after a subset of C or Java. ## Building across chapters The optional [interactive graph viewer](graph/README.md) is shared in `graph/`. -Chapters 18–25 launch it with `make view` from the chapter directory. +Chapters 10a, 10b, and 18–25 launch it with `make view` from the chapter directory. The top-level Makefile runs each chapter's `tests`, `tags` (also `tag`), `release`, or `lib` target. Start with `make lib` on a fresh checkout, then @@ -81,4 +82,17 @@ The top-level Makefile runs each chapter's `tests`, `tags` (also `tag`), The [chapter backport queue](docs/chapter-backports.md) records proposed small corrections and the per-chapter test/review workflow. Larger architectural moves -remain deferred while that workflow is established. +are tracked separately there. The 10a/10b split is complete; forwarding 10b's +memory representation through Chapters 11-24 remains pending. Those snapshots +still use their earlier parser-managed alias chains. + +To build and test just the memory chapters: + +```sh +make lib tests release CHAPTERS="chapter10a chapter10b" +``` + +The root Maven reactor also includes both modules. Each chapter has its own +IDEA module descriptor and depends on the shared `graph` module. Chapter +directories with letter suffixes participate in the normal Make chapter discovery +and in the linear-history workflow. diff --git a/TypeAnalysis.md b/TypeAnalysis.md index ea51f41d..425f6dbc 100644 --- a/TypeAnalysis.md +++ b/TypeAnalysis.md @@ -305,7 +305,7 @@ but we will shortly use them in building structs and functions. [Lattice6](./docs/lattice_iffct.svg) -First appears in [Simple chapter10](https://github.com/SeaOfNodes/Simple/tree/main/chapter10) +First appears in [Simple chapter10a](https://github.com/SeaOfNodes/Simple/tree/main/chapter10a) along with structs and pointers. @@ -337,7 +337,7 @@ desired semantics, as well as the normal *meet* on the field type. I am not covering here how to initialize struct members, or the syntax for declaring structs or traits - just the resulting types. -First appears in [Simple chapter10](https://github.com/SeaOfNodes/Simple/tree/main/chapter10) +First appears in [Simple chapter10a](https://github.com/SeaOfNodes/Simple/tree/main/chapter10a) along with tuples and pointers. @@ -377,7 +377,7 @@ Here the load to fetch the `makeSound` method fails during error reporting; in Simple this is just a `LoadNode` which has an error check against its pointer input being possibly `null`. -First appears in [Simple chapter10](https://github.com/SeaOfNodes/Simple/tree/main/chapter10), +First appears in [Simple chapter10a](https://github.com/SeaOfNodes/Simple/tree/main/chapter10a), along with structs and tuples. diff --git a/chapter02/README.md b/chapter02/README.md index c670ea36..eda25269 100644 --- a/chapter02/README.md +++ b/chapter02/README.md @@ -93,7 +93,7 @@ optimizations such as: * The compiler may be able to identify regions of code that are dead and no longer needed, such as when a conditional branch always take one of the branches ([Chapter 6](../chapter06/README.md)). -* Pointers may be known to be not-null, and a null check can be skipped ([Chapter 10](../chapter10/README.md)). +* Pointers may be known to be not-null, and a null check can be skipped ([Chapter 10a](../chapter10a/README.md)). * Array indices may be known to be in-range, and a range check can be skipped. * Many additional optimizations are possible when the compiler learns more about the possible set of Node values. @@ -151,7 +151,7 @@ starting in [Chapter 4](../chapter04/README.md) and [Chapter There are other important properties of the Lattice that we discuss in [Chapter -4](../chapter04/README.md) and [Chapter 10](../chapter10/README.md), such as the "meet" and "join" operators and their rules. +4](../chapter04/README.md) and [Chapter 10a](../chapter10a/README.md), such as the "meet" and "join" operators and their rules. ## Nodes Pre Peephole Optimization diff --git a/chapter03/README.md b/chapter03/README.md index 39baf903..e8944e12 100644 --- a/chapter03/README.md +++ b/chapter03/README.md @@ -72,9 +72,9 @@ A ScopeNode encapsulates the stack of symbol tables that are used to implement l * In [Chapter 5](../chapter05/README.md) when we implement branches, the ScopeNode gets duplicated at a branch point and then gets merged when the branch joins. This allows the names to evolve in each branch independently. At the merge point names that have diverged require a Phi node. * In [Chapter 7](../chapter07/README.md) additional logic is deployed for handling Phis from back edges. -* The ScopeNode is used to track some other values such as the Control token starting from [Chapter 4](../chapter04/README.md), and Aliases starting from [Chapter 10](../chapter10/README.md). Special name bindings are used +* The ScopeNode is used to track some other values such as the Control token starting from [Chapter 4](../chapter04/README.md), and whole memory starting from [Chapter 10a](../chapter10a/README.md). Special name bindings are used for these, and each name is tracked across branches and then merged at merge points. -* In [Chapter 10](../chapter10/README.md) the ScopeNode is enhanced to also maintain the declared type for each name. +* In [Chapter 10a](../chapter10a/README.md) the ScopeNode is enhanced to also maintain the declared type for each name. From an implementation standpoint, each symbol table is represented as a `HashMap`, where the `String` is the variable name, and the `Integer` is an index into the inputs of the ScopeNode. To access the Node associated with a name, first the symbol table is consulted, and then the ScopeNode's inputs to find the required Node. diff --git a/chapter10/Makefile b/chapter10/Makefile deleted file mode 100644 index 77df7182..00000000 --- a/chapter10/Makefile +++ /dev/null @@ -1,105 +0,0 @@ -SHELL := /bin/bash -# Shared packaging target; included after chapter source variables below. -.DELETE_ON_ERROR: - -# for printing variable values -# usage: make print-VARIABLE -# > VARIABLE = value_of_variable -print-% : ; @echo $* = $($*) - -# literal space -space := $() $() - -# Decide OS-specific questions -# jar-file seperator -ifeq ($(OS),Windows_NT) - SEP = ; -else - SEP = : -endif -# Find a reasonable ctags. -CTAGS = $(shell which ctags) -# Hack for MacOS: /usr/bin/ctags is unfriendly, so look for ctags from brew -ifeq ($(UNAME),Darwin) - CTAGS = $(shell brew list ctags 2> /dev/null | grep bin/ctags) -endif - -# Fun Args to javac. Mostly limit to java8 source definitions, and fairly -# aggressive lint warnings. -JAVAC_ARGS = -g - -# Source code -SIMPLE := com/seaofnodes/simple -SRC := src/main/java -TST := src/test/java -CLZDIR:= build/classes -main_javas := $(wildcard $(SRC)/$(SIMPLE)/*java $(SRC)/$(SIMPLE)/*/*java) -test_javas := $(wildcard $(TST)/$(SIMPLE)/*java $(TST)/$(SIMPLE)/*/*java) -main_classes := $(patsubst $(SRC)/%java,$(CLZDIR)/main/%class,$(main_javas)) -include $(firstword $(wildcard ../graph/graph.mk graph/graph.mk)) -test_classes := $(patsubst $(TST)/%java,$(CLZDIR)/test/%class,$(test_javas)) -test_cp := $(patsubst $(TST)/$(SIMPLE)/%.java,com.seaofnodes.simple.%,$(wildcard $(TST)/$(SIMPLE)/*Test.java)) -classes = $(main_classes) $(test_classes) -# All the libraries -libs = $(wildcard lib/*jar) -jars = $(subst $(space),$(SEP),$(libs)) - - -default_targets := $(main_classes) $(test_classes) -# Optionally add ctags to the default target if a reasonable one was found. -ifneq ($(CTAGS),) -default_targets += tags -endif - -default: $(default_targets) - -# Compile just the out-of-date files -$(main_classes): build/classes/main/%class: $(SRC)/%java $(graph_javas) - @echo "compiling " $@ " because " $? - @[ -d $(CLZDIR)/main ] || mkdir -p $(CLZDIR)/main - @javac $(JAVAC_ARGS) -cp "$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(SRC) -d $(CLZDIR)/main $(main_javas) - -$(test_classes): $(CLZDIR)/test/%class: $(TST)/%java $(main_classes) - @echo "compiling " $@ " because " $? - @[ -d $(CLZDIR)/test ] || mkdir -p $(CLZDIR)/test - @javac $(JAVAC_ARGS) -cp "$(CLZDIR)/test$(SEP)$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(TST) -d $(CLZDIR)/test $(test_javas) - -# Base launch line for JVM tests -JVM=nice java -ea -cp "build/classes/main${SEP}${jars}${SEP}$(CLZDIR)/test" - -tests: $(default_targets) - @echo "testing " $(test_cp) - @$(JVM) org.junit.runner.JUnitCore $(test_cp) com.seaofnodes.simple.FuzzerWrap - -fuzzer: $(default_targets) - @echo "fuzzing " $(test_cp) - @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap - - -.PHONY: clean -clean: - rm -rf build - rm -f TAGS - (find . -name "*~" -exec rm {} \; 2>/dev/null; exit 0) - -# Download libs from maven -lib: lib/junit-4.12.jar lib/hamcrest-core-1.3.jar - -# Jars -lib/junit-4.12.jar: - @[ -d lib ] || mkdir -p lib - @(cd lib; wget https://repo1.maven.org/maven2/junit/junit/4.12/junit-4.12.jar) - -lib/hamcrest-core-1.3.jar: - @[ -d lib ] || mkdir -p lib - @(cd lib; wget https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar) - -# Build emacs tags (part of a tasty emacs ide experience) -tags: $(main_javas) $(test_javas) - @rm -f TAGS - @$(CTAGS) -e --recurse=yes --extra=+q --fields=+fksaiS $(SRC) $(TST) - -include $(firstword $(wildcard ../build-support/chapter-release.mk build-support/chapter-release.mk)) - -.PHONY: build -build: $(main_classes) diff --git a/chapter10/README.md b/chapter10/README.md deleted file mode 100644 index 88f0016a..00000000 --- a/chapter10/README.md +++ /dev/null @@ -1,359 +0,0 @@ -# Chapter 10: Structs and Memory - - -# Table of Contents - -1. [Memory](#memory) -2. [Alias Classes](#alias-classes) -3. [Implementation of Alias Classes](#implementation-of-alias-classes) -4. [Extensions to Intermediate Representation ](#extensions-to-intermediate-representation) -5. [Enhanced Type Lattice](#enhanced-type-lattice) -6. [Parser Enhancements](#parser-enhancements) -7. [Null Pointer Analysis](#null-pointer-analysis) -8. [Memory Operations Example](#memory-operations-example) -9. [Additional Peephole Optimizations](#additional-peephole-optimizations) - -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. - -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](docs/10-grammar.md) for this 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](./docs/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](./docs/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](./docs/example2a.svg) - -After enabling Store-Store peephole we get: - -![Graph2b](./docs/example2b.svg) - -After load-after-store elision: - -![Graph2c](./docs/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/chapter10/docs/10-grammar.md b/chapter10/docs/10-grammar.md deleted file mode 100644 index b76531d5..00000000 --- a/chapter10/docs/10-grammar.md +++ /dev/null @@ -1,125 +0,0 @@ -# Grammar for Chapter 10 - -```antlrv4 -grammar SimpleLanguage; - -program - : statement+ EOF - ; - -statement - : returnStatement - | structDeclaration - | declStatement - | blockStatment - | expressionStatement - | ifStatement - | whileStatement - | breakStatement - | continueStatment - ; - -field - : 'int' IDENTIFIER ';' - ; - -fields - : field+ - ; - -structDeclaration - : 'struct' IDENTIFIER '{' fields '}' - ; - -whileStatement - : 'while' '(' expression ')' statement - ; - -breakStatement - : 'break' ';' - ; - -continueStatement - : 'continue' ';' - ; - -ifStatement - : 'if' '(' expression ')' statement ('else' statement)? - ; - - -expressionStatement - : IDENTIFIER '=' expression ';' - | fieldExpression '=' expression ';' - ; - -blockStatement - : '{' statement+ '}' - ; - -structName - : IDENTIFIER - ; - -declStatement - : 'int' IDENTIFIER '=' expression ';' - | structName ('?')? IDENTIFIER '=' expression ';' - ; - -returnStatement - : 'return' expression ';' - ; - -expression - : comparisonExpression - ; - -comparisonExpression - : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* - ; - -additiveExpression - : multiplicativeExpression (('+' | '-') multiplicativeExpression)* - ; - -multiplicativeExpression - : unaryExpression (('*' | '/') unaryExpression)* - ; - -unaryExpression - : ('-') unaryExpression - | '!' unaryExpression - | primaryExpression - ; - -newExpression - : 'new' IDENTIFIER - ; - -fieldExpression - : primaryExpresson '.' IDENTIFIER - ; - -primaryExpression - : IDENTIFIER - | INTEGER_LITERAL - | 'true' - | 'false' - | 'null' - | newExpression - | '(' expression ')' - | fieldExpression - ; - -INTEGER_LITERAL - : [1-9][0-9]* - | [0] - ; - -IDENTIFIER - : NON_DIGIT (NON_DIGIT | DIGIT)* - ; - -NON_DIGIT: [a-zA-Z_]; -DEC_DIGIT: [0-9]; -``` diff --git a/chapter10/docs/Makefile b/chapter10/docs/Makefile deleted file mode 100644 index 12659b8f..00000000 --- a/chapter10/docs/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -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/chapter10/docs/example1.gv b/chapter10/docs/example1.gv deleted file mode 100644 index cbb12cb5..00000000 --- a/chapter10/docs/example1.gv +++ /dev/null @@ -1,78 +0,0 @@ -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/chapter10/docs/example1.svg b/chapter10/docs/example1.svg deleted file mode 100644 index 6f493e7e..00000000 --- a/chapter10/docs/example1.svg +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - -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/chapter10/docs/example2a.gv b/chapter10/docs/example2a.gv deleted file mode 100644 index 23f31bf8..00000000 --- a/chapter10/docs/example2a.gv +++ /dev/null @@ -1,65 +0,0 @@ -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/chapter10/docs/example2a.svg b/chapter10/docs/example2a.svg deleted file mode 100644 index 9f069ad4..00000000 --- a/chapter10/docs/example2a.svg +++ /dev/null @@ -1,249 +0,0 @@ - - - - - - -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/chapter10/docs/example2b.gv b/chapter10/docs/example2b.gv deleted file mode 100644 index d732e874..00000000 --- a/chapter10/docs/example2b.gv +++ /dev/null @@ -1,56 +0,0 @@ -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/chapter10/docs/example2b.svg b/chapter10/docs/example2b.svg deleted file mode 100644 index 1ecf4de7..00000000 --- a/chapter10/docs/example2b.svg +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - -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/chapter10/docs/example2c.gv b/chapter10/docs/example2c.gv deleted file mode 100644 index 76db03bb..00000000 --- a/chapter10/docs/example2c.gv +++ /dev/null @@ -1,53 +0,0 @@ -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/chapter10/docs/example2c.svg b/chapter10/docs/example2c.svg deleted file mode 100644 index d70660a2..00000000 --- a/chapter10/docs/example2c.svg +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - -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/chapter10/docs/lattice.drawio b/chapter10/docs/lattice.drawio deleted file mode 100644 index cf20e9f3..00000000 --- a/chapter10/docs/lattice.drawio +++ /dev/null @@ -1,379 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/chapter10/docs/lattice.gv b/chapter10/docs/lattice.gv deleted file mode 100644 index 7723e2a2..00000000 --- a/chapter10/docs/lattice.gv +++ /dev/null @@ -1,161 +0,0 @@ -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/chapter10/docs/lattice.svg b/chapter10/docs/lattice.svg deleted file mode 100644 index 9eb36bbe..00000000 --- a/chapter10/docs/lattice.svg +++ /dev/null @@ -1,516 +0,0 @@ - - - - - - -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/chapter10/pom.xml b/chapter10/pom.xml deleted file mode 100644 index 6c4500aa..00000000 --- a/chapter10/pom.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - 4.0.0 - - com.seaofnodes - simple - 1.0 - - chapter10 - jar - Chapter 10 - - ${project.basedir}/../graph - - diff --git a/chapter10/src/main/java/com/seaofnodes/simple/IRPrinter.java b/chapter10/src/main/java/com/seaofnodes/simple/IRPrinter.java deleted file mode 100644 index 1ac32b1a..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/IRPrinter.java +++ /dev/null @@ -1,188 +0,0 @@ -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 { - - // Print a node on 1 line, columnar aligned, as: - // NNID NNAME DDEF DDEF [[ UUSE UUSE ]] TYPE - // 1234 sssss 1234 1234 1234 1234 1234 1234 tttttt - public static void _printLine( Node n, StringBuilder sb ) { - sb.append("%4d %-7.7s ".formatted(n._nid,n.label())); - if( n.isDead() ) { - sb.append("DEAD\n"); - return; - } - for( Node def : n._inputs ) - sb.append(def==null ? "____ " : "%4d ".formatted(def._nid)); - for( int i = n._inputs.size(); i<3; i++ ) - sb.append(" "); - sb.append(" [[ "); - for( Node use : n._outputs ) - sb.append(use==null ? "____ " : "%4d ".formatted(use._nid)); - int lim = 5 - Math.max(n._inputs.size(),3); - for( int i = n._outputs.size(); i 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 - ArrayList rpos = new ArrayList<>(); - BitSet visit = new BitSet(); - for( int i=bfs._lim; i< bfs._bfs.size(); i++ ) - postOrd( bfs._bfs.get(i), rpos, visit, bfs._bs); - // Reverse the post-order walk - StringBuilder sb = new StringBuilder(); - boolean gap=false; - for( int i=rpos.size()-1; i>=0; i-- ) { - Node n = rpos.get(i); - if( n.isCFG() || n.isMultiHead() ) { - if( !gap ) sb.append("\n"); // Blank before multihead - printLine( n, sb, llvmFormat ); // Print head - while( --i >= 0 ) { - Node t = rpos.get(i); - if( !t.isMultiTail() ) { i++; break; } - printLine( t, sb, llvmFormat ); - } - sb.append("\n"); // Blank after multitail - gap = true; - } else { - printLine( n, sb, llvmFormat ); - gap = false; - } - } - return sb.toString(); - } - - private static void postOrd(Node n, ArrayList rpos, BitSet visit, BitSet bfs) { - if( !bfs.get(n._nid) ) - return; // Not in the BFS visit - if( visit.get(n._nid) ) return; // Already post-order walked - visit.set(n._nid); - // First walk the CFG, then everything - if( n.isCFG() ) { - for( Node use : n._outputs ) - if( use!=null && use.isCFG() && use.nOuts()>=1 && !(use._outputs.get(0) instanceof LoopNode) ) - postOrd(use, rpos,visit,bfs); - for( Node use : n._outputs ) - if( use!=null && use.isCFG() ) - postOrd(use,rpos,visit,bfs); - } - for( Node use : n._outputs ) - if( use != null ) - postOrd(use, rpos,visit,bfs); - // Post-order - rpos.add(n); - } - - // Breadth-first search, broken out in a class to keep in more independent. - // Maintains a root-set of Nodes at the limit (or past by 1 if MultiHead). - public static class BFS { - // A breadth first search, plus MultiHeads for any MultiTails - public final ArrayList _bfs; - public final BitSet _bs; // Visited members by node id - public final int _depth; // Depth limit - public final int _lim; // From here to _bfs._len can be roots for a reverse search - public BFS( Node base, int d ) { - _depth = d; - _bfs = new ArrayList<>(); - _bs = new BitSet(); - - add(base); // Prime the pump - int idx=0, lim=1; // Limit is where depth counter changes - while( idx < _bfs.size() ) { // Ran out of nodes below depth - Node n = _bfs.get(idx++); - for( Node def : n._inputs ) - if( def!=null && !_bs.get(def._nid) ) - add(def); - if( idx==lim ) { // Depth counter changes at limit - if( --d < 0 ) - break; // Ran out of depth - lim = _bfs.size(); // New depth limit - } - } - // Toss things past the limit except multi-heads - while( idx < _bfs.size() ) { - Node n = _bfs.get(idx); - if( n.isMultiHead() ) idx++; - else del(idx); - } - // Root set is any node with no inputs in the visited set - lim = _bfs.size(); - for( int i=_bfs.size()-1; i>=0; i-- ) - if( !any_visited(_bfs.get(i)) ) - swap( i,--lim); - _lim = lim; - } - void swap( int x, int y ) { - if( x==y ) return; - Node tx = _bfs.get(x); - Node ty = _bfs.get(y); - _bfs.set(x,ty); - _bfs.set(y,tx); - } - void add(Node n) { - _bfs.add(n); - _bs.set(n._nid); - } - void del(int idx) { - Node n = Utils.del(_bfs, idx); - _bs.clear(n._nid); - } - boolean any_visited( Node n ) { - for( Node def : n._inputs ) - if( def!=null && _bs.get(def._nid) ) - return true; - return false; - } - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/IterPeeps.java b/chapter10/src/main/java/com/seaofnodes/simple/IterPeeps.java deleted file mode 100644 index 7075d526..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/IterPeeps.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.*; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.BitSet; -import java.util.Random; - -/** - * The IterOptim runs after parsing. It iterates the peepholes to a fixed point - * so that no more peepholes apply. This should be linear because peepholes rarely - * (never?) increase code size. The graph should monotonically reduce in some - * dimension, which is usually size. It might also reduce in e.g. number of - * MulNodes or Load/Store nodes, swapping out more "expensive" Nodes for cheaper - * ones. - * - * The theoretical overall worklist is mindless just grabbing the next thing and - * doing it. If the graph changes, put the neighbors on the worklist. Lather, - * Rinse, Repeat until the worklist runs dry. - * - * The main issues we have to deal with: - * - *
    - *
  • Nodes have uses; replacing some set of Nodes with another requires more graph - * reworking. Not rocket science, but it can be fiddly. Its helpful to have a - * small set of graph munging utilities, and the strong invariant that the graph - * is stable and correct between peepholes. In our case `Node.subsume` does - * most of the munging, building on our prior stable Node utilities.
  • - * - *
  • Changing a Node also changes the graph "neighborhood". The neigbors need to - * be checked to see if THEY can also peephole, and so on. After any peephole - * or graph update we put a Nodes uses and defs on the worklist.
  • - * - *
  • Our strong invariant is that for all Nodes, either they are on the worklist - * OR no peephole applies. This invariant is easy to check, although expensive. - * Basically the normal "iterate peepholes to a fixed point" is linear, and this - * check is linear at each peephole step... so quadratic overall. Its a useful - * assert, but one we can disable once the overall algorithm is stable - and - * then turn it back on again when some new set of peepholes is misbehaving. - * The code for this is turned on in `IterOptim.iterate` as `assert - * progressOnList(stop);`
  • - *
- */ -public abstract class IterPeeps { - - private static final WorkList WORK = new WorkList<>(); - - public static N add( N n ) { - return (N)WORK.push(n); - } - - public static void addAll( ArrayList ary ) { - WORK.addAll(ary); - } - - /** - * Iterate peepholes to a fixed point - */ - public static StopNode iterate(StopNode stop) { - assert progressOnList(stop); - int cnt=0; - - Node n; - while( (n=WORK.pop()) != null ) { - if( n.isDead() ) continue; - cnt++; // Useful for debugging, searching which peephole broke things - var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; - if( obs != null ) obs.before(n); - Node x = n.peepholeOpt(); - if( x != null ) { - if( x.isDead() ) { - if( obs != null ) obs.after(n, x, true); - continue; - } - // peepholeOpt can return brand-new nodes, needing an initial type set - if( x._type==null ) x.setType(x.compute()); - // Changes require neighbors onto the worklist - if( x != n || !(x instanceof ConstantNode) ) { - // All outputs of n (changing node) not x (prior existing node). - for( Node z : n._outputs ) WORK.push(z); - // Everybody gets a free "go again" in case they didn't get - // made in their final form. - WORK.push(x); - // If the result is not self, revisit all inputs (because - // there's a new user), and replace in the graph. - if( x != n ) { - for( Node z : n. _inputs ) WORK.push(z); - n.subsume(x); - } - } - // If there are distant neighbors, move to worklist - n.moveDepsToWorklist(); - assert progressOnList(stop); // Very expensive assert - } - if( n.isUnused() && !(n instanceof StopNode) ) - n.kill(); // Just plain dead - if( obs != null ) obs.after(n, x, true); - } - - return stop; - } - - // Visit ALL nodes and confirm the invariant: - // Either you are on the WORK worklist OR running `iter()` makes no progress. - - // This invariant ensures that no progress is missed, i.e., when the - // worklist is empty we have indeed done all that can be done. To help - // with debugging, the {@code assert} is broken out in a place where it is easy to - // stop if a change is found. - - // Also, the normal usage of `iter()` may attempt peepholes with distance - // neighbors and these should fail, but will then try to add dependencies - // {@link #Node.addDep} which is a side effect in an assert. The {@link - // #midAssert} is used to stop this side effect. - private static boolean MID_ASSERT; - public static boolean midAssert() { return MID_ASSERT; } - private static boolean progressOnList(Node stop) { - MID_ASSERT = true; - int old_cnt = Node.ITER_CNT, old_nop = Node.ITER_NOP_CNT; - Node changed = stop.walk( n -> { - if( WORK.on(n) ) return null; - Node m = n.peepholeOpt(); - if( m==null ) return null; - System.err.println("BREAK HERE FOR BUG"); - return m; - }); - Node.ITER_CNT = old_cnt; Node.ITER_NOP_CNT = old_nop; - MID_ASSERT = false; - return changed==null; - } - - public static void reset() { - WORK.clear(); - } - - /** - * Classic WorkList, with a fast add/remove, dup removal, random pull. - * The Node's nid is used to check membership in the worklist. - */ - public static class WorkList { - - private Node[] _es; - private int _len; - private final BitSet _on; // Bit set if Node._nid is on WorkList - private final Random _R; // For randomizing pull from the WorkList - private final long _seed; - - /* Useful stat - how many nodes are processed in the post parse iterative opt */ - private long _totalWork = 0; - - WorkList() { this(123); } - WorkList(long seed) { - _es = new Node[1]; - _len=0; - _on = new BitSet(); - _seed = seed; - _R = new Random(); - _R.setSeed(_seed); - } - - /** - * Pushes a Node on the WorkList, ensuring no duplicates - * If Node is null it will not be added. - */ - public E push( E x ) { - if( x==null ) return null; - int idx = x._nid; - if( !_on.get(idx) ) { - _on.set(idx); - if( _len==_es.length ) - _es = Arrays.copyOf(_es,_len<<1); - _es[_len++] = x; - _totalWork++; - } - return x; - } - - public void addAll( ArrayList ary ) { - for( E n : ary ) - push(n); - } - - /** - * True if Node is on the WorkList - */ - boolean on( E x ) { return _on.get(x._nid); } - - /** - * Removes a random Node from the WorkList; null if WorkList is empty - */ - E pop() { - if( _len == 0 ) return null; - int idx = _R.nextInt(_len); - E x = (E)_es[idx]; - _es[idx] = _es[--_len]; // Compress array - _on.clear(x._nid); - return x; - } - - public void clear() { - _len = 0; - _on.clear(); - _R.setSeed(_seed); - _totalWork = 0; - } - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/Parser.java b/chapter10/src/main/java/com/seaofnodes/simple/Parser.java deleted file mode 100644 index ddbbdf82..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/Parser.java +++ /dev/null @@ -1,834 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.graph.GraphObserver; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.*; - -import java.util.*; - -/** - * The Parser converts a Simple source program to the Sea of Nodes intermediate - * representation directly in one pass. There is no intermediate Abstract - * Syntax Tree structure. - *

- * This is a simple recursive descent parser. All lexical analysis is done here as well. - */ -public class Parser { - - // Current compilation context; the observer belongs to the parser. - public static Parser PARSER; - public GraphObserver _obs; - public int pos() { return _lexer._position; } - - - /** - * A Global Static, unique to each compilation. This is a public, so we - * can make constants everywhere without having to thread the StartNode - * through the entire parser and optimizer. - *

- * To make the compiler multithreaded, this field will have to move into a TLS. - */ - public static StartNode START; - - public StopNode STOP; - - // The Lexer. Thin wrapper over a byte[] buffer with a cursor. - private final Lexer _lexer; - - /** - * Current ScopeNode - ScopeNodes change as we parse code, but at any point of time - * there is one current ScopeNode. The reason the current ScopeNode can change is to do with how - * we handle branching. See {@link #parseIf()}. - *

- * Each ScopeNode contains a stack of lexical scopes, each scope is a symbol table that binds - * variable names to Nodes. The top of this stack represents current scope. - *

- * We keep a list of all ScopeNodes so that we can show them in graphs. - * @see #parseIf() - * @see #_xScopes - */ - public ScopeNode _scope; - - /** - * List of keywords disallowed as identifiers - */ - 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"); - }}; - - - /** - * We clone ScopeNodes when control flows branch; it is useful to have - * a list of all active ScopeNodes for purposes of visualization of the SoN graph - */ - public final Stack _xScopes = new Stack<>(); - - ScopeNode _continueScope; - ScopeNode _breakScope; - - // Mapping from a Struct name to a Struct. - public static Map OBJS = new HashMap<>(); - - public Parser(String source, TypeInteger arg) { - PARSER = this; - Node.reset(); - IterPeeps.reset(); - OBJS.clear(); - _lexer = new Lexer(source); - _scope = new ScopeNode(); - _continueScope = _breakScope = null; - START = new StartNode(new Type[]{ Type.CONTROL, arg }); - STOP = new StopNode(source); - } - - public Parser(String source) { - this(source, TypeInteger.BOT); - } - - @Override - public String toString() { return _lexer.toString(); } - - String src() { return new String( _lexer._input ); } - - // Debugging utility to find a Node by index - public static Node find(int nid) { return START.find(nid); } - - private Node ctrl() { return _scope.ctrl(); } - - private Node ctrl(Node n) { return _scope.ctrl(n); } - - public StopNode parse() { - _xScopes.push(_scope); - // Enter a new scope for the initial control and arguments - _scope.push(); - _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(); - if (!_lexer.isEOF()) throw error("Syntax error, unexpected " + _lexer.getAnyNextToken()); - STOP.peephole(); - return STOP; - } - - /** - * Parses a block - * - *

-     *     '{' statements '}'
-     * 
- * Does not parse the opening or closing '{}' - * @return a {@link Node} or {@code null} - */ - private Node parseBlock() { - // Enter a new scope - _scope.push(); - while (!peek('}') && !_lexer.isEOF()) - parseStatement(); - // Exit scope - _scope.pop(); - return null; - } - - /** - * Parses a statement - * - *
-     *     returnStatement | declStatement | blockStatement | ifStatement | expressionStatement
-     * 
- * @return a {@link Node} or {@code null} - */ - private Node parseStatement() { - 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(";") ) 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 - * - *
-     *     while ( expression ) statement
-     * 
- * @return a {@link Node}, never {@code null} - */ - private Node parseWhile() { - - var savedContinueScope = _continueScope; - var savedBreakScope = _breakScope; - - require("("); - - // Loop region has two control inputs, the first is the entry - // point, and second is back edge that is set after loop is parsed - // (see end_loop() call below). Note that the absence of back edge is - // used as an indicator to switch off peepholes of the region and - // associated phis; see {@code inProgress()}. - - ctrl(new LoopNode(ctrl()).peephole()); // Note we set back edge to null here - - // At loop head, we clone the current Scope (this includes all - // names in every nesting level within the Scope). - // We create phis eagerly for all the names we find, see dup(). - - // Save the current scope as the loop head - ScopeNode head = _scope.keep(); - // Clone the head Scope to create a new Scope for the body. - // Create phis eagerly as part of cloning - _xScopes.push(_scope = _scope.dup(true)); // The true argument triggers creating phis - - // Parse predicate - var pred = require(parseExpression(), ")"); - // IfNode takes current control and predicate - Node ifNode = new IfNode(ctrl(), pred).peephole(); - // Setup projection nodes - Node ifT = new ProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); - Node ifF = new ProjNode(ifNode.unkeep(), 1, "False").peephole(); - - // Clone the body Scope to create the break/exit Scope which accounts for any - // side effects in the predicate. The break/exit Scope will be the final - // scope after the loop, and its control input is the False branch of - // the loop predicate. Note that body Scope is still our current scope. - ctrl(ifF); - _xScopes.push(_breakScope = _scope.dup()); - - // No continues yet - _continueScope = null; - - // Parse the true side, which corresponds to loop body - // Our current scope is the body Scope - ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection - parseStatement(); // Parse loop body - - // Merge the loop bottom into other continue statements - if (_continueScope != null) { - _continueScope = jumpTo(_continueScope); - _scope.kill(); - _scope = _continueScope; - } - - // The true branch loops back, so whatever is current _scope.ctrl gets - // added to head loop as input. endLoop() updates the head scope, and - // goes through all the phis that were created earlier. For each phi, - // it sets the second input to the corresponding input from the back - // edge. If the phi is redundant, it is replaced by its sole input. - var exit = _breakScope; - head.endLoop(_scope, exit); - head.unkeep().kill(); - - _xScopes.pop(); // Cleanup - _xScopes.pop(); // Cleanup - - _continueScope = savedContinueScope; - _breakScope = savedBreakScope; - - // At exit the false control is the current control, and - // the scope is the exit scope after the exit test. - return _scope = exit; - } - - private ScopeNode jumpTo(ScopeNode toScope) { - ScopeNode cur = _scope.dup(); - ctrl(new ConstantNode(Type.XCONTROL).peephole()); // Kill current scope - // Prune nested lexical scopes that have depth > than the loop head - // We use _breakScope as a proxy for the loop head scope to obtain the depth - while( cur._scopes.size() > _breakScope._scopes.size() ) - 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" - if (toScope == null) - return cur; - // toScope is either the break scope, or a scope that was created here - assert toScope._scopes.size() <= _breakScope._scopes.size(); - toScope.ctrl(toScope.mergeScopes(cur)); - return toScope; - } - - private void checkLoopActive() { if (_breakScope == null) throw Parser.error("No active loop for a break or continue"); } - - private Node parseBreak () { checkLoopActive(); return ( _breakScope = require(jumpTo( _breakScope ),";")); } - private Node parseContinue() { checkLoopActive(); return (_continueScope = require(jumpTo( _continueScope ),";")); } - - /** - * Parses a statement - * - *
-     *     if ( expression ) statement [else statement]
-     * 
- * @return a {@link Node}, never {@code null} - */ - private Node parseIf() { - require("("); - // Parse predicate - var pred = require(parseExpression(), ")").keep(); - // IfNode takes current control and predicate - Node ifNode = new IfNode(ctrl(), pred).peephole(); - // Setup projection nodes - Node ifT = new ProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); - Node ifF = new ProjNode(ifNode.unkeep(), 1, "False").peephole().keep(); - // In if true branch, the ifT proj node becomes the ctrl - // But first clone the scope and set it as current - int ndefs = _scope.nIns(); - ScopeNode fScope = _scope.dup(); // Duplicate current scope - fScope.ctrl(ifF); // The pending arm already starts at False - _xScopes.push(fScope); // For graph visualization we need all scopes - - // 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; - - // Parse the false side - _scope = fScope; // Restore scope, then parse else block if any - ifF.unkeep(); // fScope already owns the false control - if (matchx("else")) { - _scope.upcast(ifF,pred,true); // Up-cast predicate - parseStatement(); - fScope = _scope; - } - pred.unkeep(); - - if( tScope.nIns() != ndefs || fScope.nIns() != ndefs ) - throw error("Cannot define a new name on one arm of an if"); - - // Merge results - _scope = tScope; - _xScopes.pop(); // Discard pushed from graph display - - return ctrl(tScope.mergeScopes(fScope)); - } - - - /** - * Parses a return statement; "return" already parsed. - * The $ctrl edge is killed. - * - *
-     *     'return' expr ;
-     * 
- * @return an expression {@link Node}, never {@code null} - */ - private Node parseReturn() { - var expr = require(parseExpression(), ";"); - Node ret = STOP.addReturn(new ReturnNode(ctrl(), expr, _scope).peephole()); - ctrl(new ConstantNode(Type.XCONTROL).peephole()); // Kill control - return ret; - } - - - - /** - * Parses an expression statement or a declaration statement where type is a struct - * - *
-     *      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() { - 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 - * - *
-     *     type name = expression ';'
-     * 
- * @return an expression {@link Node} - */ - private Node parseDecl(Type t) { - var name = requireId(); - 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: - * - *
-     *     expr : compareExpr
-     * 
- * @return an expression {@link Node}, never {@code null} - */ - private Node parseExpression() { return parseComparison(); } - - /** - * Parse an expression of the form: - * - *
-     *     expr : additiveExpr op additiveExpr
-     * 
- * @return an comparator expression {@link Node}, never {@code null} - */ - private Node parseComparison() { - var lhs = parseAddition(); - while( true ) { - int idx=0; boolean negate=false; - // Test for any local nodes made, and "keep" lhs during peepholes - if( false ) ; - else if( match("==") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); } - else if( match("!=") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); negate=true; } - else if( match("<=") ) { idx=2; lhs = new BoolNode.LE(lhs, null); } - else if( match("<" ) ) { idx=2; lhs = new BoolNode.LT(lhs, null); } - else if( match(">=") ) { idx=1; lhs = new BoolNode.LE(null, lhs); } - else if( match(">" ) ) { idx=1; lhs = new BoolNode.LT(null, lhs); } - else break; - // Peepholes can fire, but lhs is already "hooked", kept alive - lhs.setDef(idx,parseAddition()); - lhs = lhs.peephole(); - if( negate ) // Extra negate for != - lhs = new NotNode(lhs).peephole(); - } - return lhs; - } - - /** - * Parse an additive expression - * - *
-     *     additiveExpr : multiplicativeExpr (('+' | '-') multiplicativeExpr)*
-     * 
- * @return an add expression {@link Node}, never {@code null} - */ - private Node parseAddition() { - Node lhs = parseMultiplication(); - while( true ) { - if( false ) ; - else if( match("+") ) lhs = new AddNode(lhs,null); - else if( match("-") ) lhs = new SubNode(lhs,null); - else break; - lhs.setDef(2,parseMultiplication()); - lhs = lhs.peephole(); - } - return lhs; - } - - /** - * Parse an multiplicativeExpr expression - * - *
-     *     multiplicativeExpr : unaryExpr (('*' | '/') unaryExpr)*
-     * 
- * @return a multiply expression {@link Node}, never {@code null} - */ - private Node parseMultiplication() { - var lhs = parseUnary(); - while( true ) { - if( false ) ; - else if( match("*") ) lhs = new MulNode(lhs,null); - else if( match("/") ) lhs = new DivNode(lhs,null); - else break; - lhs.setDef(2,parseUnary()); - lhs = lhs.peephole(); - } - return lhs; - } - - /** - * Parse a unary minus expression. - * - *
-     *     unaryExpr : ('-') | '!') unaryExpr | postfixExpr | primaryExpr
-     * 
- * @return a unary expression {@link Node}, never {@code null} - */ - private Node parseUnary() { - if (match("-")) return new MinusNode(parseUnary()).peephole(); - if (match("!")) return new NotNode(parseUnary()).peephole(); - return parsePostfix(parsePrimary()); - } - - /** - * Parse a primary expression: - * - *
-     *     primaryExpr : integerLiteral | Identifier | true | false | null | new Identifier | '(' expression ')'
-     * 
- * @return a primary {@link Node}, never {@code null} - */ - private Node parsePrimary() { - if( _lexer.isNumber() ) return parseIntegerLiteral(); - 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); - if( n!=null ) return n; - 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 - * - *
-     *     integerLiteral: [1-9][0-9]* | [0]
-     * 
- */ - private ConstantNode parseIntegerLiteral() { - return (ConstantNode) new ConstantNode(_lexer.parseNumber()).peephole(); - } - - ////////////////////////////////// - // Utilities for lexical analysis - - // Return true and skip if "syntax" is next in the stream. - private boolean match (String syntax) { return _lexer.match (syntax); } - // Match must be "exact", not be followed by more id letters - 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() { - String id = _lexer.matchId(); - if (id != null && !KEYWORDS.contains(id) ) return id; - throw error("Expected an identifier, found '"+id+"'"); - } - - - - // Require an exact match - 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); - } - - RuntimeException errorSyntax(String syntax) { - return error("Syntax error, expected " + syntax + ": " + _lexer.getAnyNextToken()); - } - - public static RuntimeException error( String errorMessage ) { - return new RuntimeException(errorMessage); - } - - //////////////////////////////////// - // Lexer components - - private static class Lexer { - - // Input buffer; an array of text bytes read from a file or a string - private final byte[] _input; - // Tracks current position in input buffer - private int _position = 0; - - /** - * Record the source text for lexing - */ - public Lexer(String source) { - this(source.getBytes()); - } - - /** - * Direct from disk file source - */ - public Lexer(byte[] buf) { - _input = buf; - } - - // Very handy in the debugger, shows the unparsed program - @Override - public String toString() { - return new String(_input, _position, _input.length - _position); - } - - // True if at EOF - private boolean isEOF() { - return _position >= _input.length; - } - - // Peek next character, or report EOF - private char peek() { - return isEOF() ? Character.MAX_VALUE // Special value that causes parsing to terminate - : (char) _input[_position]; - } - - private char nextChar() { - char ch = peek(); - _position++; - return ch; - } - - // True if a white space - private boolean isWhiteSpace() { - return peek() <= ' '; // Includes all the use space, tab, newline, CR - } - - /** - * Return the next non-white-space character - */ - private void skipWhiteSpace() { - while (isWhiteSpace()) _position++; - } - - - // Return true, if we find "syntax" after skipping white space; also - // then advance the cursor past syntax. - // Return false otherwise, and do not advance the cursor. - boolean match(String syntax) { - skipWhiteSpace(); - int len = syntax.length(); - if (_position + len > _input.length) return false; - for (int i = 0; i < len; i++) - if ((char) _input[_position + i] != syntax.charAt(i)) - return false; - _position += len; - 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; - _position -= syntax.length(); - return false; - } - - private boolean peek(char ch) { - skipWhiteSpace(); - return peek()==ch; - } - - boolean peekIsId() { - skipWhiteSpace(); - return isIdStart(peek()); - } - - // Return an identifier or null - String matchId() { - return peekIsId() ? parseId() : null; - } - - // Used for errors - String getAnyNextToken() { - if (isEOF()) return ""; - if (isIdStart(peek())) return parseId(); - if (isNumber(peek())) return parseNumberString(); - if (isPunctuation(peek())) return parsePunctuation(); - return String.valueOf(peek()); - } - - boolean isNumber() {return isNumber(peek());} - boolean isNumber(char ch) {return Character.isDigit(ch);} - - private Type parseNumber() { - String snum = parseNumberString(); - if (snum.length() > 1 && snum.charAt(0) == '0') - throw error("Syntax error: integer values cannot start with '0'"); - return TypeInteger.constant(Long.parseLong(snum)); - } - private String parseNumberString() { - int start = _position; - while (isNumber(nextChar())) ; - return new String(_input, start, --_position - start); - } - - // First letter of an identifier - private boolean isIdStart(char ch) { - return Character.isAlphabetic(ch) || ch == '_'; - } - - // All characters of an identifier, e.g. "_x123" - private boolean isIdLetter(char ch) { - return Character.isLetterOrDigit(ch) || ch == '_'; - } - - private String parseId() { - int start = _position; - while (isIdLetter(nextChar())) ; - return new String(_input, start, --_position - start); - } - - private boolean isPunctuation(char ch) { - return "=;[]<>(){}+-/*!".indexOf(ch) != -1; - } - - private String parsePunctuation() { - int start = _position; - return new String(_input, start, 1); - } - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/Utils.java b/chapter10/src/main/java/com/seaofnodes/simple/Utils.java deleted file mode 100644 index 4f2af769..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/Utils.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.seaofnodes.simple; - -import java.util.ArrayList; - -public class Utils { - public static RuntimeException TODO() { return TODO("Not yet implemented"); } - public static RuntimeException TODO(String msg) { return new RuntimeException(msg); } - - /** - * Fast, constant-time, element removal. Does not preserve order - * - * @param array ArrayList to modify - * @param i element to be removed - * @return element removed - */ - public static E del(ArrayList array, int i) { - if ( i >= 0 && i < array.size() ) { - E tmp = array.get(i); - E last = array.removeLast(); - if (i < array.size()) array.set(i, last); - return tmp; - } - return null; - } - - /** - * 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( ArrayList ary, E x ) { - for( int i=0; i= 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/chapter10/src/main/java/com/seaofnodes/simple/node/AddNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/AddNode.java deleted file mode 100644 index de5044d7..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/AddNode.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.*; - -import java.util.BitSet; - -public class AddNode extends Node { - public AddNode(Node lhs, Node rhs) { super(null, lhs, rhs); } - - @Override public String label() { return "Add"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - in(1)._print0(sb.append("("), visited); - in(2)._print0(sb.append("+"), visited); - return sb.append(")"); - } - - - @Override - public Type compute() { - Type t1 = in(1)._type, t2 = in(2)._type; - if( t1==Type.TOP || t1==TypeInteger.TOP || - t2==Type.TOP || t2==TypeInteger.TOP ) - return TypeInteger.TOP; - if( t1 instanceof TypeInteger i0 && - t2 instanceof TypeInteger i1 ) { - if (i0.isConstant() && i1.isConstant()) - return TypeInteger.constant(i0.value()+i1.value()); - } - return TypeInteger.BOT; - } - - @Override - public Node idealize () { - Node lhs = in(1); - Node rhs = in(2); - Type t2 = rhs._type; - - // Add of 0. We do not check for (0+x) because this will already - // canonicalize to (x+0) - if( t2 instanceof TypeInteger i && i.value()==0 ) - return lhs; - - // Add of same to a multiply by 2 - if( lhs==rhs ) - return new MulNode(lhs,new ConstantNode(TypeInteger.constant(2)).peephole()); - - // Goal: a left-spine set of adds, with constants on the rhs (which then fold). - - // Move non-adds to RHS - if( !(lhs instanceof AddNode) && rhs instanceof AddNode ) - return swap12(); - - // x+(-y) becomes x-y - if( rhs instanceof MinusNode minus ) - return new SubNode(lhs,minus.in(1)); - - // Now we might see (add add non) or (add non non) or (add add add) but never (add non add) - - // Do we have x + (y + z) ? - // Swap to (x + y) + z - // Rotate (add add add) to remove the add on RHS - if( rhs instanceof AddNode add ) - return new AddNode(new AddNode(lhs,add.in(1)).peephole(), add.in(2)); - - // Now we might see (add add non) or (add non non) but never (add non add) nor (add add add) - if( !(lhs instanceof AddNode) ) - // Rotate; look for (add (phi cons) con/(phi cons)) - return spine_cmp(lhs,rhs,this) ? swap12() : phiCon(this,true); - - // Now we only see (add add non) - - // Dead data cycle; comes about from dead infinite loops. Do nothing, - // the loop will peep as dead after a bit. - if( lhs.in(1) == lhs ) - return null; - - // Do we have (x + con1) + con2? - // Replace with (x + (con1+con2) which then fold the constants - // lhs.in(2) is con1 here - // If lhs.in(2) is not a constant, we add ourselves as a dependency - // because if it later became a constant then we could make this - // transformation. - if( lhs.in(2).addDep(this)._type.isConstant() && rhs._type.isConstant() ) - return new AddNode(lhs.in(1),new AddNode(lhs.in(2),rhs).peephole()); - - - // 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 = phiCon(this,true); - if( phicon!=null ) return phicon; - - // Now we sort along the spine via rotates, to gather similar things together. - - // Do we rotate (x + y) + z - // into (x + z) + y ? - if( spine_cmp(lhs.in(2),rhs,this) ) - return new AddNode(new AddNode(lhs.in(1),rhs).peephole(),lhs.in(2)); - - return null; - } - - // Rotation is only valid for associative ops, e.g. Add, Mul, And, Or. - // Do we have ((phi cons)|(x + (phi cons)) + con|(phi cons)) ? - // Push constant up through the phi: x + (phi con0+con0 con1+con1...) - static Node phiCon(Node op, boolean rotate) { - Node lhs = op.in(1); - Node rhs = op.in(2); - // LHS is either a Phi of constants, or another op with Phi of constants - PhiNode lphi = pcon(lhs,op); - if( rotate && lphi==null && lhs.nIns() > 2 ) { - // Only valid to rotate constants if both are same associative ops - if( lhs.getClass() != op.getClass() ) return null; - lphi = pcon(lhs.in(2),op); // Will rotate with the Phi push - } - if( lphi==null ) return null; - - // RHS is a constant or a Phi of constants - if( !(rhs instanceof ConstantNode) && pcon(rhs,op)==null ) - return null; - - // If both are Phis, must be same Region - if( rhs instanceof PhiNode && lphi.in(0) != rhs.in(0) ) - return null; - - // Note that this is the exact reverse of Phi pulling a common op down - // to reduce total op-count. We don't get in an endless push-up - // push-down peephole cycle because the constants all fold first. - Node[] ns = new Node[lphi.nIns()]; - ns[0] = lphi.in(0); - // Push constant up through the phi: x + (phi con0+con0 con1+con1...) - for( int i=1; i hi._nid; - } - - @Override Node copy(Node lhs, Node rhs) { return new AddNode(lhs,rhs); } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/BoolNode.java deleted file mode 100644 index bf0d3a84..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/BoolNode.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; -import com.seaofnodes.simple.type.TypeMemPtr; - -import java.util.BitSet; - -abstract public class BoolNode extends Node { - - public BoolNode(Node lhs, Node rhs) { - super(null, lhs, rhs); - } - - abstract String op(); // String opcode name - - @Override - public String label() { return getClass().getSimpleName(); } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - in(1)._print0(sb.append("("), visited); - in(2)._print0(sb.append(op()), visited); - return sb.append(")"); - } - - @Override - public Type compute() { - Type t1 = in(1)._type, t2 = in(2)._type; - if( t1==Type.TOP || t1==TypeInteger.TOP || - t2==Type.TOP || t2==TypeInteger.TOP ) - return TypeInteger.TOP; - if( t1 instanceof TypeInteger i0 && - t2 instanceof TypeInteger i1 ) { - if (i0.isConstant() && i1.isConstant()) - return TypeInteger.constant(doOp(i0.value(), i1.value()) ? 1 : 0); - } - return TypeInteger.BOT; - } - - abstract boolean doOp(long lhs, long rhs); - - @Override - public Node idealize() { - // Compare of same - if( in(1)==in(2) ) - return new ConstantNode(TypeInteger.constant(doOp(3,3)?1:0)); - - // Equals pushes constant to the right; 5==X becomes X==5. - if( this instanceof EQ ) { - if( !(in(2) instanceof ConstantNode) ) { - // con==noncon becomes noncon==con - if( in(1) instanceof ConstantNode ) - return new EQ(in(2),in(1)); - // Equals sorts by NID otherwise: non.high == non.low becomes non.low == non.high - else if( in(1)._nid > in(2)._nid ) - return new EQ(in(2),in(1)); - } - // Equals X==0 becomes a !X - if( (in(2)._type == TypeInteger.ZERO || in(2)._type == TypeMemPtr.NULLPTR) ) - return new NotNode(in(1)); - } - - // Do we have ((phi cons) cmp con) ? - // Do we have ((phi cons) cmp (phi cons)) ? - // Push the compare up through the phi: (phi con0 cmp con, con1 cmp con...) - Node phicon = AddNode.phiCon(this,this instanceof EQ); - if( phicon!=null ) return phicon; - - return null; - } - - public static class EQ extends BoolNode { public EQ(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "=="; } boolean doOp(long lhs, long rhs) { return lhs == rhs; } Node copy(Node lhs, Node rhs) { return new EQ(lhs,rhs); } } - public static class LT extends BoolNode { public LT(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "<" ; } boolean doOp(long lhs, long rhs) { return lhs < rhs; } Node copy(Node lhs, Node rhs) { return new LT(lhs,rhs); } } - public static class LE extends BoolNode { public LE(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "<="; } boolean doOp(long lhs, long rhs) { return lhs <= rhs; } Node copy(Node lhs, Node rhs) { return new LE(lhs,rhs); } } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/CastNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/CastNode.java deleted file mode 100644 index fec540f7..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/CastNode.java +++ /dev/null @@ -1,38 +0,0 @@ -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/chapter10/src/main/java/com/seaofnodes/simple/node/ConstantNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/ConstantNode.java deleted file mode 100644 index d760c0df..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/ConstantNode.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.type.Type; - -import java.util.BitSet; - -/** - * A Constant node represents a constant value. At present, the only constants - * that we allow are integer literals; therefore Constants contain an integer - * value. As we add other types of constants, we will refactor how we represent - * Constants. - *

- * Constants have no semantic inputs. However, we set Start as an input to - * Constants to enable a forward graph walk. This edge carries no semantic - * meaning, and it is present solely to allow visitation. - *

- * The Constant's value is the value stored in it. - */ -public class ConstantNode extends Node { - final Type _con; - public ConstantNode( Type type ) { - super(Parser.START); - _con = type; - } - - @Override - public String label() { return "#"+_con; } - - @Override - public String uniqueName() { return "Con_" + _nid; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - return _con.print(sb); - } - - @Override public boolean isMultiTail() { return true; } - - @Override - public Type compute() { return _con; } - - @Override - public Node idealize() { return null; } - - @Override - boolean eq(Node n) { - ConstantNode con = (ConstantNode)n; // Contract - return _con==con._con; - } - - @Override - int hash() { return _con.hashCode(); } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/DivNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/DivNode.java deleted file mode 100644 index 6cee90ad..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/DivNode.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; - -import java.util.BitSet; - -public class DivNode extends Node { - public DivNode(Node lhs, Node rhs) { super(null, lhs, rhs); } - - @Override public String label() { return "Div"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - in(1)._print0(sb.append("("), visited); - in(2)._print0(sb.append("/"), visited); - return sb.append(")"); - } - - @Override - public Type compute() { - Type t1 = in(1)._type, t2 = in(2)._type; - if( t1==Type.TOP || t1==TypeInteger.TOP || - t2==Type.TOP || t2==TypeInteger.TOP ) - return TypeInteger.TOP; - if (t1 instanceof TypeInteger i0 && - t2 instanceof TypeInteger i1) { - if (i0.isConstant() && i1.isConstant()) - return i1.value() == 0 - ? TypeInteger.ZERO - : TypeInteger.constant(i0.value()/i1.value()); - } - return TypeInteger.BOT; - } - - @Override - public Node idealize() { - // Div of 1. - if( in(2)._type.isConstant() && in(2)._type instanceof TypeInteger i && i.value()==1 ) - return in(1); - return null; - } - - @Override Node copy(Node lhs, Node rhs) { return new DivNode(lhs,rhs); } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/IfNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/IfNode.java deleted file mode 100644 index 1cf4ddaf..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/IfNode.java +++ /dev/null @@ -1,62 +0,0 @@ -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 - public String label() { return "If"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - sb.append("if( "); - return in(1)._print0(sb, visited).append(" )"); - } - - @Override public boolean isCFG() { return true; } - @Override public boolean isMultiHead() { return true; } - - public Node ctrl() { return in(0); } - public Node pred() { return in(1); } - - @Override - public Type compute() { - // If the If node is not reachable then neither is any following Proj - if (ctrl()._type != Type.CONTROL && ctrl()._type != Type.BOTTOM ) - return TypeTuple.IF_NEITHER; - Node pred = pred(); - Type t = pred._type; - // High types mean NEITHER side is reachable. - // Wait until the type falls to decide which way to go. - if( t == Type.TOP || t == TypeInteger.TOP ) - return TypeTuple.IF_NEITHER; - // If constant is 0 then false branch is reachable - // Else true branch is reachable - if (t instanceof TypeInteger ti && ti.isConstant()) - return ti==TypeInteger.ZERO ? TypeTuple.IF_FALSE : TypeTuple.IF_TRUE; - - return TypeTuple.IF_BOTH; - } - - @Override - public Node idealize() { - // Hunt up the immediate dominator tree. If we find an identical if - // test on either the true or false branch, that side wins. - if( !pred()._type.isHighOrConst() ) - for( Node dom = idom(), prior=this; dom!=null; prior = dom, dom = dom.idom() ) - if( dom.addDep(this) instanceof IfNode iff && iff.pred().addDep(this)==pred() && prior instanceof ProjNode prj ) { - setDef(1,new ConstantNode(TypeInteger.make(true,prj._idx==0?1:0)).peephole()); - return this; - } - return null; - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/LoadNode.java deleted file mode 100644 index ece27d86..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/LoadNode.java +++ /dev/null @@ -1,75 +0,0 @@ -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 - 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/chapter10/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/LoopNode.java deleted file mode 100644 index a0ba973e..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/LoopNode.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; - -import java.util.BitSet; - -public class LoopNode extends RegionNode { - - public LoopNode( Node entry ) { super(null,entry,null); } - - Node entry() { return in(1); } - Node back () { return in(2); } - - @Override - public String label() { return "Loop"; } - - @Override - public Type compute() { - if( inProgress() ) return Type.CONTROL; - return entry()._type; - } - - @Override - public Node idealize() { - return inProgress() ? null : super.idealize(); - } - - // Bypass Region idom, same as the default idom() using use entry in(1) instead of in(0) - @Override int idepth() { return _idepth!=0 ? _idepth : cacheIDepth(entry().idepth()+1); } - @Override Node idom() { return entry(); } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/MemOpNode.java deleted file mode 100644 index 08e08bbf..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/MemOpNode.java +++ /dev/null @@ -1,40 +0,0 @@ -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/chapter10/src/main/java/com/seaofnodes/simple/node/MinusNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/MinusNode.java deleted file mode 100644 index 8b1b73bf..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/MinusNode.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; - -import java.util.BitSet; - -public class MinusNode extends Node { - public MinusNode(Node in) { super(null, in); } - - @Override public String label() { return "Minus"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - in(1)._print0(sb.append("(-"), visited); - return sb.append(")"); - } - - @Override - public Type compute() { - if (in(1)._type instanceof TypeInteger i0) - return i0.isConstant() ? TypeInteger.constant(-i0.value()) : i0; - return in(1)._type==Type.TOP ? TypeInteger.TOP : TypeInteger.BOT; - } - - @Override - public Node idealize() { - // -(-x) is x - if( in(1) instanceof MinusNode minus ) - return minus.in(1); - - return null; - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/MulNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/MulNode.java deleted file mode 100644 index 97721ce3..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/MulNode.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; - -import java.util.BitSet; - -public class MulNode extends Node { - public MulNode(Node lhs, Node rhs) { super(null, lhs, rhs); } - - @Override public String label() { return "Mul"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - in(1)._print0(sb.append("("), visited); - in(2)._print0(sb.append("*"), visited); - return sb.append(")"); - } - - @Override - public Type compute() { - Type t1 = in(1)._type, t2 = in(2)._type; - if( t1==Type.TOP || t1==TypeInteger.TOP || - t2==Type.TOP || t2==TypeInteger.TOP ) - return TypeInteger.TOP; - if (t1 instanceof TypeInteger i0 && - t2 instanceof TypeInteger i1) { - if (i0.isConstant() && i1.isConstant()) - return TypeInteger.constant(i0.value()*i1.value()); - } - return TypeInteger.BOT; - } - - @Override - public Node idealize() { - Node lhs = in(1); - Node rhs = in(2); - Type t1 = lhs._type; - Type t2 = rhs._type; - - // Mul of 1. We do not check for (1*x) because this will already - // canonicalize to (x*1) - if ( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==1 ) - return lhs; - - // Move constants to RHS: con*arg becomes arg*con - if ( t1.isConstant() && !t2.isConstant() ) - return swap12(); - - // 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,true); - if( phicon!=null ) return phicon; - - return null; - } - @Override Node copy(Node lhs, Node rhs) { return new MulNode(lhs,rhs); } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/MultiNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/MultiNode.java deleted file mode 100644 index 2743bd19..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/MultiNode.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.seaofnodes.simple.node; - -public abstract class MultiNode extends Node { - - public MultiNode(Node... inputs) { super(inputs); } - - // Find a projection by index - ProjNode proj( int idx ) { - for( Node out : _outputs ) - if( out instanceof ProjNode prj && prj._idx==idx ) - return prj; - return null; - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/NewNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/NewNode.java deleted file mode 100644 index b13462f5..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/NewNode.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeMemPtr; - -import java.util.BitSet; - -public class NewNode extends Node { - - TypeMemPtr _ptr; - - public NewNode(TypeMemPtr ptr, Node ctrl) { - super(ctrl); - this._ptr = ptr; - } - - @Override - public String label() { return "new"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - sb.append("new "); - return sb.append(_ptr._obj.str()); - } - - @Override - public Type compute() { return _ptr; } - - @Override - public Node idealize() { return null; } - - @Override - boolean eq(Node n) { return this == n; } - - @Override - int hash() { return _ptr.hashCode(); } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter10/src/main/java/com/seaofnodes/simple/node/Node.java deleted file mode 100644 index eb6c1697..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/Node.java +++ /dev/null @@ -1,633 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.Parser; - -import com.seaofnodes.simple.IRPrinter; -import com.seaofnodes.simple.IterPeeps; -import com.seaofnodes.simple.Utils; -import com.seaofnodes.simple.type.Type; - -import java.util.*; -import java.util.function.Function; - -/** - * All Nodes in the Sea of Nodes IR inherit from the Node class. - * The Node class provides common functionality used by all subtypes. - * Subtypes of Node specialize by overriding methods. - */ -public abstract class Node { - - /** - * Each node has a unique dense Node ID within a compilation context - * The ID is useful for debugging, for using as an offset in a bitvector, - * as well as for computing equality of nodes (to be implemented later). - */ - public final int _nid; - - /** - * Inputs to the node. These are use-def references to Nodes. - *

- * Generally fixed length, ordered, nulls allowed, no unused trailing space. - * Ordering is required because e.g. "a/b" is different from "b/a". - * The first input (offset 0) is often a {@link #isCFG} node. - */ - public final ArrayList _inputs; - - /** - * Outputs reference Nodes that are not null and have this Node as an - * input. These nodes are users of this node, thus these are def-use - * references to Nodes. - *

- * Outputs directly match inputs, making a directed graph that can be - * walked in either direction. These outputs are typically used for - * efficient optimizations but otherwise have no semantics meaning. - */ - public final ArrayList _outputs; - - - /** - * Current computed type for this Node. This value changes as the graph - * changes and more knowledge is gained about the program. - */ - public Type _type; - - - /** - * A private Global Static mutable counter, for unique node id generation. - * To make the compiler multithreaded, this field will have to move into a TLS. - * Starting with value 1, to avoid bugs confusing node ID 0 with uninitialized values. - * */ - private static int UNIQUE_ID = 1; - public static int UID() { return UNIQUE_ID; } - - protected Node(Node... inputs) { - _nid = UNIQUE_ID++; // allocate unique dense ID - _inputs = new ArrayList<>(); - Collections.addAll(_inputs,inputs); - _outputs = new ArrayList<>(); - for( Node n : _inputs ) - if( n != null ) - n.addUse( this ); - } - - // Easy reading label for debugger, e.g. "Add" or "Region" or "EQ" - public abstract String label(); - - // Unique label for debugging, e.g. "Add12" or "Region30" or "EQ99" - public String uniqueName() { - return label() + _nid; - } - - // ------------------------------------------------------------------------ - - // Debugger Printing. - - // {@code toString} is what you get in the debugger. It has to print 1 - // line (because this is what a debugger typically displays by default) and - // has to be robust with broken graph/nodes. - @Override - public final String toString() { return print(); } - - // This is a *deep* print. This version will fail on cycles, which we will - // correct later when we can parse programs with loops. We print with a - // tik-tok style; the common _print0 calls the per-Node _print1, which - // calls back to _print0; - public final String print() { - return _print0(new StringBuilder(), new BitSet()).toString(); - } - - // This is the common print: check for repeats, check for DEAD and print - // "DEAD" else call the per-Node print1. - final StringBuilder _print0(StringBuilder sb, BitSet visited) { - if (visited.get(_nid) && !(this instanceof ConstantNode) ) - return sb.append(label()); - visited.set(_nid); - return isDead() - ? sb.append(uniqueName()).append(":DEAD") - : _print1(sb, visited); - } - // Every Node implements this; a partial-line recursive print - abstract StringBuilder _print1(StringBuilder sb, BitSet visited); - - public String p(int depth) { return IRPrinter.prettyPrint(this,depth); } - - public boolean isMultiHead() { return false; } - public boolean isMultiTail() { return false; } - - // ------------------------------------------------------------------------ - // Graph Node & Edge manipulation - - /** - * Gets the ith input node - * @param i Offset of the input node - * @return Input node or null - */ - public Node in(int i) { return _inputs.get(i); } - public Node out(int i) { return _outputs.get(i); } - - public int nIns() { return _inputs.size(); } - - public int nOuts() { return _outputs.size(); } - - public boolean isUnused() { return nOuts() == 0; } - - public boolean isCFG() { return false; } - - public boolean isMem() { return false; } - - /** - * Change a def into a Node. Keeps the edges correct, by removing - * the corresponding use->def edge. This may make the original - * def go dead. This function is co-recursive with {@link #kill}. - *

- - * This method is the normal path for altering a Node, because it does the - * proper default edge maintenance. It also immediately kills - * Nodes that lose their last use; at times care must be taken to avoid - * killing Nodes that are being used without having an output Node. This - * definitely happens in the middle of recursive {@link #peephole} calls. - * - * @param idx which def to set - * @param new_def the new definition - * @return new_def for flow coding - */ - public Node setDef(int idx, Node new_def ) { - unlock(); - Node old_def = in(idx); - if( old_def == new_def ) return new_def; // No change - // If new def is not null, add the corresponding def->use edge - // This needs to happen before removing the old node's def->use edge as - // the new_def might get killed if the old node kills it recursively. - if( new_def != null ) - new_def.addUse(this); - if( old_def != null && // If the old def exists, remove a def->use edge - old_def.delUse(this) ) // If we removed the last use, the old def is now dead - 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; - } - - // Remove the numbered input, compressing the inputs in-place. This - // shuffles the order deterministically - which is suitable for Region and - // Phi, but not for every Node. If the def goes dead, it is recursively - // killed, which may include 'this' Node. - Node delDef(int idx) { - unlock(); - Node old_def = in(idx); - if( old_def != null && // If the old def exists, remove a def->use edge - old_def.delUse(this) ) // If we removed the last use, the old def is now dead - old_def.kill(); // Kill old def - old_def.moveDepsToWorklist(); - Utils.del(_inputs, idx); - return this; - } - - /** - * Add a new def to an existing Node. Keep the edges correct by - * adding the corresponding def->use edge. - * - * @param new_def the new definition, appended to the end of existing definitions - * @return new_def for flow coding - */ - Node addDef(Node new_def) { - unlock(); - // Add use->def edge - _inputs.add(new_def); - // If new def is not null, add the corresponding def->use edge - if( new_def != null ) - new_def.addUse(this); - return new_def; - } - - // Breaks the edge invariants, used temporarily - protected void addUse(N n) { _outputs.add(n); } - - // Remove node 'use' from 'def's (i.e. our) output list, by compressing the list in-place. - // Return true if the output list is empty afterward. - // Error is 'use' does not exist; ok for 'use' to be null. - protected boolean delUse( Node use ) { - Utils.del(_outputs, Utils.find(_outputs, use)); - return _outputs.isEmpty(); - } - - // Shortcut for "popping" n nodes. A "pop" is basically a - // setDef(last,null) followed by lowering the nIns() count. - void popN(int n) { - unlock(); - for( int i=0; iuses, by setting all of its defs - * to null. This may recursively kill more Nodes, and is basically dead - * code elimination. - */ - public void kill( ) { - unlock(); - assert isUnused(); // Has no uses, so it is dead - _type=null; // Flag as dead - while( nIns()>0 ) { // Set all inputs to null, recursively killing unused Nodes - Node old_def = _inputs.removeLast(); - if( old_def != null ) { - IterPeeps.add(old_def);// Revisit neighbor because removed use - if( old_def.delUse(this) ) // If we removed the last use, the old def is now dead - old_def.kill(); // Kill old def - } - } - assert isDead(); // Really dead now - } - - // Mostly used for asserts and printing. - public boolean isDead() { return isUnused() && nIns()==0 && _type==null; } - - // Shortcuts to stop DCE mid-parse - // Add bogus null use to keep node alive - public N keep() { addUse(null); return (N)this; } - // Remove bogus null. - 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 - public void subsume( Node nnn ) { - assert nnn!=this; - while( nOuts() > 0 ) { - Node n = _outputs.removeLast(); - n.unlock(); - int idx = Utils.find(n._inputs, this); - n._inputs.set(idx,nnn); - nnn.addUse(n); - } - kill(); - } - - // ------------------------------------------------------------------------ - // Graph-based optimizations - - /** - * Try to peephole at this node and return a better replacement Node. - * Always returns some not-null Node (often this). - */ - public final Node peephole() { - var obs = IterPeeps.midAssert() ? null : Parser.PARSER == null ? null : Parser.PARSER._obs; - if( obs != null ) obs.before(this); - Node n = peepholeOpt(); - Node rez = n == null ? this : deadCodeElim(n.peephole()); - if( obs != null ) obs.after(this, n == null ? null : rez, false); - return rez; - } - - /** - * Try to peephole at this node and return a better replacement Node if - * possible. We compute a {@link Type} and then check and replace: - *

    - *
  • if the Type {@link Type#isConstant}, we replace with a {@link ConstantNode}
  • - *
  • in a future chapter we will look for a - * Common Subexpression - * to eliminate.
  • - *
  • we ask the Node for a better replacement. The "better replacement" - * is things like {@code (1+2)} becomes {@code 3} and {@code (1+(x+2))} becomes - * {@code (x+(1+2))}. By canonicalizing expressions we fold common addressing - * math constants, remove algebraic identities and generally simplify the - * code.
  • - * - * Unlike peephole above, this explicitly returns null for no-change, or not-null - * for a better replacement (which can be this). - *
- */ - public final Node peepholeOpt( ) { - ITER_CNT++; - // Compute initial or improved Type - Type old = setType(compute()); - - // Replace constant computations from non-constants with a constant node - if( !(this instanceof ConstantNode) && _type.isHighOrConst() ) - return new ConstantNode(_type).peepholeOpt(); - - // Global Value Numbering - if( _hash==0 ) { - Node n = GVN.get(this); // Will set _hash as a side effect - if( n==null ) - GVN.put(this,this); // Put in table now - else { - // Because of random worklist ordering, the two equal nodes - // might have different types. Because of monotonicity, both - // types are valid. To preserve monotonicity, the resulting - // shared Node has to have the best of both types. - n.setType(n._type.join(_type)); - _hash = 0; // Clear, since it never went in the table - return deadCodeElim(n);// Return previous; does Common Subexpression Elimination - } - } - - // Ask each node for a better replacement - Node n = idealize(); - if( n != null ) // Something changed - return n; // Report progress - - if( old==_type ) ITER_NOP_CNT++; - return old==_type ? null : this; // Report progress - } - public static int ITER_CNT, ITER_NOP_CNT; - - // m is the new Node, self is the old. - // Return 'm', which may have zero uses but is alive nonetheless. - // If self has zero uses (and is not 'm'), {@link #kill} self. - 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() && !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(). - m.keep(); // Keep m alive - kill(); // Kill self because replacing with 'm' - m.unkeep(); // Okay to peephole m - } - return m; - } - - /** - * This function needs to be - * Monotonic - * as it is part of a Monotone Analysis Framework. - * See for example this set of slides. - *

- * For Chapter 2, all our Types are really integer constants, and so all - * the needed properties are trivially true, and we can ignore the high - * theory. Much later on, this will become important and allow us to do - * many fancy complex optimizations trivially... because theory. - *

- * {@link #compute()} needs to be stand-alone, and cannot recursively call compute - * on its inputs, because programs are cyclic (have loops!) and this will just - * infinitely recurse until stack overflow. Instead, compute typically - * computes a new type from the {@link #_type} field of its inputs. - */ - public abstract Type compute(); - - // Set the type. Assert monotonic progress. - // If changing, add users to worklist. - public Type setType(Type type) { - Type old = _type; - assert old==null || type.isa(old); // Since _type not set, can just re-run this in assert in the debugger - if( old == type ) return old; - _type = type; // Set _type late for easier assert debugging - IterPeeps.addAll(_outputs); - moveDepsToWorklist(); - return old; - } - - - /** - * This function rewrites the current Node into a more "idealized" form. - * This is the bulk of our peephole rewrite rules, and we use this to - * e.g. turn arbitrary collections of adds and multiplies with mixed - * constants into a normal form that's easy for hardware to implement. - * Example: An array addressing expression: - *

   ary[idx+1]
- * might turn into Sea-of-Nodes IR: - *
   (ary+12)+((idx+1) * 4)
- * This expression can then be idealized into: - *
   ary + ((idx*4) + (12 + (1*4)))
- * And more folding: - *
   ary + ((idx<<2) + 16)
- * And during code-gen: - *
   MOV4 Rary,Ridx,16 // or some such hardware-specific notation 
- *

- * {@link #idealize} has a very specific calling convention: - *

    - *
  • If NO change is made, return {@code null} - *
  • If ANY change is made, return not-null; this can be {@code this} - *
  • The returned Node does NOT call {@link #peephole} on itself; the {@link #peephole} call will recursively peephole it. - *
  • Any NEW nodes that are not directly returned DO call {@link #peephole}. - *
- *

- * Examples: - * - * - * - * - * - *
before after return comment
{@code (x+5) }{@code (x+5) }{@code null }No change
{@code (5+x) }{@code (x+5) }{@code this }Swapped arguments
{@code ((x+1)+2)}{@code (x+(1+2))}{@code (x+_) }Returns 2 new Nodes
- * - * The last entry deserves more discussion. The new Node {@code (1+2)} - * created in {@link #idealize} calls {@link #peephole} (which then folds - * into a constant). The other new Node {@code (x+3)} does not call - * peephole, because it is returned and peephole itself will recursively - * call peephole. - *

- * Since idealize calls peephole and peephole calls idealize, you must be - * careful that all idealizations are monotonic: all transforms - * remove some feature, so that the set of available transforms always - * shrinks. If you don't, you risk an infinite peephole loop! - * - * @return Either a new or changed node, or null for no changes. - */ - public abstract Node idealize(); - - - // Some of the peephole rules get complex, and search further afield than - // just the nearest neighbor. These peepholes can fail the pattern match - // on a node some distance away, and if that node ever changes we should - // retry the peephole. Track a set of Nodes dependent on `this`, and - // revisit them if `this` changes. - ArrayList _deps; - public int nDeps() { return _deps == null ? 0 : _deps.size(); } - public Node dep(int idx) { return _deps.get(idx); } - - /** - * 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 - // like adding dependencies. - if( IterPeeps.midAssert() ) return this; - var obs = IterPeeps.midAssert() ? null : Parser.PARSER == null ? null : Parser.PARSER._obs; - if( obs != null ) obs.dep(this, dep); - if( _deps==null ) _deps = new ArrayList<>(); - if( Utils.find(_deps ,dep) != -1 ) return this; // Already on list - if( Utils.find(_inputs,dep) != -1 ) return this; // No need for deps on immediate neighbors - if( Utils.find(_outputs,dep)!= -1 ) return this; - _deps.add(dep); - return this; - } - - // Move the dependents onto a worklist, and clear for future dependents. - public void moveDepsToWorklist( ) { - if( _deps==null ) return; - IterPeeps.addAll(_deps); - _deps.clear(); - } - - - // Global Value Numbering. Hash over opcode and inputs; hits in this table - // are structurally equal. - public static final HashMap GVN = new HashMap<>(); - - // Two nodes are equal if they have the same inputs and the same "opcode" - // which means the same Java class, plus same internal parts. - @Override public final boolean equals( Object o ) { - if( o==this ) return true; - if( o.getClass() != getClass() ) return false; - Node n = (Node)o; - int len = _inputs.size(); - if( len != n._inputs.size() ) return false; - for( int i=0; i>13) ^ n._nid; - if( hash==0 ) hash = 0xDEADBEEF; // Bad hash, so use some junky thing - return (_hash=hash); - } - // Subclasses add extra hash info (such as ConstantNodes constant) - int hash() { return 0; } - - - // ------------------------------------------------------------------------ - // Peephole utilities - - // Swap inputs without letting either input go dead during the swap. - Node swap12() { - unlock(); // Hash is order dependent - Node tmp = in(1); - _inputs.set(1,in(2)); - _inputs.set(2,tmp); - return this; - } - - /** - * 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 char _idepth; // IDOM depth approx; Zero is unset; non-zero is cached legit - - // Find the lowest common ancestor in the current dominator tree. - Node domLCA(Node rhs) { - if( rhs==null ) return this; - Node lhs = this; - while( lhs != rhs ) { - if( lhs==null || rhs==null ) return null; - int comp = lhs.idepth() - rhs.idepth(); - if( comp >= 0 ) lhs = lhs.idom(); - if( comp <= 0 ) rhs = rhs.idom(); - } - return lhs; - } - - int idepth() { return _idepth!=0 ? _idepth : cacheIDepth(idom().idepth()+1); } - // Zero depth means uncached. Check before narrowing so overflow cannot wrap. - final int cacheIDepth(int depth) { - assert 0 <= depth && depth <= Character.MAX_VALUE : "Dominator depth exceeds 65535"; - return _idepth = (char)depth; - } - - - // 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. - */ - public static void reset() { - UNIQUE_ID = 1; - GVN.clear(); - ITER_CNT = ITER_NOP_CNT = 0; - } - - - // Utility to walk the entire graph applying a function; return the first - // not-null result. - private static final BitSet WVISIT = new BitSet(); - final public E walk( Function pred ) { - assert WVISIT.isEmpty(); - E rez = _walk(pred); - WVISIT.clear(); - return rez; - } - - private E _walk( Function pred ) { - if( WVISIT.get(_nid) ) return null; // Been there, done that - WVISIT.set(_nid); - E x = pred.apply(this); - if( x != null ) return x; - for( Node def : _inputs ) if( def != null && (x = def._walk(pred)) != null ) return x; - for( Node use : _outputs ) if( use != null && (x = use._walk(pred)) != null ) return x; - return null; - } - - /** - * Debugging utility to find a Node by index - */ - public Node find(int nid) { - return walk( n -> n._nid==nid ? n : null ); - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/NotNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/NotNode.java deleted file mode 100644 index 83141836..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/NotNode.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.Utils; -import com.seaofnodes.simple.type.*; -import java.util.BitSet; - -public class NotNode extends Node { - public NotNode(Node in) { super(null, in); } - - @Override public String label() { return "Not"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - in(1)._print0(sb.append("(!"), visited); - return sb.append(")"); - } - - @Override - public Type compute() { - Type t0 = in(1)._type; - switch( t0 ) { - case TypeInteger i0: - return i0.isConstant() ? TypeInteger.constant(i0.value()==0 ? 1 : 0) : i0; - case TypeMemPtr p0: - // top->top, bot->bot, null->1, *void->0, not-null ptr->0, ptr/nil->bot - // If input in null then true - // If input is not null ptr then false - if( p0 == TypeMemPtr.TOP ) return TypeInteger.TOP; - if( p0 == TypeMemPtr.NULLPTR ) return TypeInteger.constant(1); - if( !p0._nil ) return TypeInteger.constant(0); - return TypeInteger.BOT; - case Type t: - if( t0.getClass() != Type.class ) - // Only doing NOT on ints and ptrs - throw Utils.TODO(); - return t0==Type.TOP ? TypeInteger.TOP : TypeInteger.BOT; - } - } - - @Override - public Node idealize() { return null; } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/PhiNode.java deleted file mode 100644 index bdf50847..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/PhiNode.java +++ /dev/null @@ -1,157 +0,0 @@ -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; - -public class PhiNode extends Node { - - final String _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; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - if( !(region() instanceof RegionNode r) || r.inProgress() ) - sb.append("Z"); - sb.append("Phi("); - for( Node in : _inputs ) { - if (in == null) sb.append("____"); - else in._print0(sb, visited); - sb.append(","); - } - sb.setLength(sb.length()-1); - sb.append(")"); - return sb; - } - - 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; - // 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 ) - t = t.meet(in(i)._type); - return t; - } - - @Override - public Node idealize() { - if( !(region() instanceof RegionNode r ) ) - return in(1); // Input has collapse to e.g. starting control. - if( r.inProgress() || r.nIns()<=1 ) - return null; // Input is in-progress - - // If we have only a single unique input, become it. - Node live = singleUniqueInput(); - if (live != null) - return live; - - // Pull "down" a common data op. One less op in the world. One more - // Phi, but Phis do not make code. - // Phi(op(A,B),op(Q,R),op(X,Y)) becomes - // op(Phi(A,Q,X), Phi(B,R,Y)). - Node op = in(1); - if( op.nIns()==3 && op.in(0)==null && !op.isCFG() && same_op() ) { - Node[] lhss = new Node[nIns()]; - Node[] rhss = new Node[nIns()]; - lhss[0] = rhss[0] = in(0); // Set Region - for( int i=1; i1 && in(nIns()-1) == null; - } - - // Never equal if inProgress - @Override boolean eq( Node n ) { - return !inProgress(); - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/ReturnNode.java deleted file mode 100644 index 92bda5bf..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/ReturnNode.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.*; - -import java.util.BitSet; - -/** - * The Return node has two inputs. The first input is a control node and the - * second is the data node that supplies the return value. - *

- * In this presentation, Return functions as a Stop node, since multiple return statements are not possible. - * The Stop node will be introduced in Chapter 6 when we implement if statements. - *

- * The Return's output is the value from the data node. - */ -public class ReturnNode extends Node { - - 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); } - public Node expr() { return in(1); } - - @Override - public String label() { return "Return"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - sb.append("return "); - expr()._print0(sb, visited); - return sb.append(";"); - } - - @Override public boolean isCFG() { return true; } - - @Override - public Type compute() { - return TypeTuple.make(ctrl()._type,expr()._type); - } - - @Override - public Node idealize() { - if( ctrl()._type==Type.XCONTROL ) - return ctrl(); - return null; - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/ScopeNode.java deleted file mode 100644 index 455f4b3a..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ /dev/null @@ -1,304 +0,0 @@ -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.*; - -/** - * The Scope node is purely a parser helper - it tracks names to nodes with a - * stack of scopes. - */ -public class ScopeNode extends Node { - - /** - * The control is a name that binds to the currently active control - * node in the graph - */ - public static final String CTRL = "$ctrl"; - public static final String ARG0 = "arg"; - - /** - * Names for every input edge - */ - 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; - } - - - @Override public String label() { return "Scope"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - sb.append("Scope[ "); - String[] names = reverseNames(); - for( int j=0; j syms : _scopes ) - for( String name : syms.keySet() ) - names[syms.get(name)] = name; - return names; - } - - @Override public Type compute() { return Type.BOTTOM; } - - @Override public Node idealize() { return null; } - - 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, 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); - } - /** - * Lookup a name in all scopes starting from most deeply nested. - * - * @param name Name to be looked up - * @see #update(String, Node, int) - */ - public Node lookup( String name ) { return update(name,null,_scopes.size()-1); } - /** - * If the name is present in any scope, then redefine else null - * - * @param name Name being redefined - * @param n The node to bind to the name - */ - 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. - * - * @param name Name whose binding is being updated or looked up - * @param n If null, do a lookup, else update binding - * @param nestingLevel The starting nesting level - * @return node being looked up, or the one that was updated - */ - private Node update( String name, Node n, int nestingLevel ) { - if( nestingLevel<0 ) return null; // Missed in all scopes, not found - var syms = _scopes.get(nestingLevel); // Get the symbol table for nesting level - var idx = syms.get(name); - if( idx == null ) return update(name,n,nestingLevel-1); // Not found in this scope, recursively look in parent scope - Node old = in(idx); - if( old instanceof ScopeNode loop ) { - // Lazy Phi! - old = loop.in(idx) instanceof PhiNode phi && loop.ctrl()==phi.region() - // Loop already has a real Phi, use it - ? loop.in(idx) - // 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, 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); } - - /** - * The ctrl of a ScopeNode is always bound to the currently active - * control node in the graph, via a special name '$ctrl' that is not - * a valid identifier in the language grammar and hence cannot be - * referenced in Simple code. - * - * @param n The node to be bound to '$ctrl' - * - * @return Node that was bound - */ - public Node ctrl(Node n) { return setDef(0,n); } - - /** - * Duplicate a ScopeNode; including all levels, up to Nodes. So this is - * neither shallow (would dup the Scope but not the internal HashMap - * tables), nor deep (would dup the Scope, the HashMap tables, but then - * also the program Nodes). - *

- * If the {@code loop} flag is set, the edges are filled in as the original - * Scope, as a indication of Lazy Phis at loop heads. The goal here is to - * not make Phis at loop heads for variables which are never touched in the - * loop body. - *

- * The new Scope is a full-fledged Node with proper use<->def edges. - */ - public ScopeNode dup() { return dup(false); } - public ScopeNode dup(boolean loop) { - ScopeNode dup = new ScopeNode(); - // Our goals are: - // 1) duplicate the name bindings of the ScopeNode across all stack levels - // 2) Make the new ScopeNode a user of all the nodes bound - // 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 - 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 - public String label() { return "Start"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - return sb.append(label()); - } - - @Override public boolean isCFG() { return true; } - @Override public boolean isMultiHead() { return true; } - - @Override - public TypeTuple compute() { return _args; } - - @Override - public Node idealize() { return null; } - - // No immediate dominator, and idepth==0 - @Override int idepth() { return 0; } - @Override Node idom() { return null; } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/StopNode.java deleted file mode 100644 index cb4126f5..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.Parser; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.IterPeeps; - -import java.util.BitSet; - -public class StopNode extends Node { - - public final String _src; - - public StopNode(String src) { - super(); - _src = src; - } - - @Override - public String label() { - return "Stop"; - } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - if( ret()!=null ) return ret()._print0(sb, visited); - sb.append("Stop[ "); - for( Node ret : _inputs ) - ret._print0(sb, visited).append(" "); - return sb.append("]"); - } - - @Override public boolean isCFG() { return true; } - - // If a single Return, return it. - // Otherwise, null because ambiguous. - public ReturnNode ret() { - return nIns()==1 ? (ReturnNode)in(0) : null; - } - - @Override - public Type compute() { - return Type.BOTTOM; - } - - @Override - public Node idealize() { - int len = nIns(); - for( int i=0; i { - String msg = n.err(); - if( msg != null && obs != null ) obs.error(n, msg); - return msg; - }); - if( err != null ) throw new RuntimeException(err); - if( obs != null ) obs.phase("TypeCheck"); - return this; - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/StoreNode.java deleted file mode 100644 index cf586d6e..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/StoreNode.java +++ /dev/null @@ -1,68 +0,0 @@ -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 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/chapter10/src/main/java/com/seaofnodes/simple/node/SubNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/SubNode.java deleted file mode 100644 index eeec5e1d..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/SubNode.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; - -import java.util.BitSet; - -public class SubNode extends Node { - public SubNode(Node lhs, Node rhs) { super(null, lhs, rhs); } - - @Override public String label() { return "Sub"; } - - @Override - StringBuilder _print1(StringBuilder sb, BitSet visited) { - in(1)._print0(sb.append("("), visited); - in(2)._print0(sb.append("-"), visited); - return sb.append(")"); - } - - @Override - public Type compute() { - Type t1 = in(1)._type, t2 = in(2)._type; - if( t1==Type.TOP || t1==TypeInteger.TOP || - t2==Type.TOP || t2==TypeInteger.TOP ) - return TypeInteger.TOP; - // Sub of same is 0 - if( in(1)==in(2) ) - return TypeInteger.ZERO; - - if (t1 instanceof TypeInteger i0 && - t2 instanceof TypeInteger i1) { - if (i0.isConstant() && i1.isConstant()) - return TypeInteger.constant(i0.value()-i1.value()); - } - return TypeInteger.BOT; - } - - @Override - public Node idealize() { - Node lhs = in(1); - Node rhs = in(2); - Type t1 = lhs._type; - Type t2 = rhs._type; - - // Keep invalid operands for type checking. - if( t1 instanceof TypeInteger x && t2 instanceof TypeInteger y ) { - if( y.isConstant() && y.value()==0 ) return lhs; - if( x.isConstant() && x.value()==0 ) return new MinusNode(rhs); - } - - // x - (-y) is x+y - if( rhs instanceof MinusNode minus ) - return new AddNode(lhs,minus.in(1)); - - // (-x) - y is -(x+y) - if( lhs instanceof MinusNode minus ) - return new MinusNode(new AddNode(minus.in(1),rhs).peephole()); - - return null; - } - - @Override Node copy(Node lhs, Node rhs) { return new SubNode(lhs,rhs); } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java deleted file mode 100644 index 600e9c73..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.seaofnodes.simple.print; - -import com.seaofnodes.graph.GraphAdapter; -import com.seaofnodes.graph.GraphSnapshot; -import com.seaofnodes.graph.GraphSnapshot.*; -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.node.Node; -import com.seaofnodes.simple.type.TypeMem; -import java.util.ArrayList; - -/** Chapter 10's view of the IR; browser, transport and layout are shared. */ -public class SimpleGraphAdapter extends GraphAdapter { - @Override protected boolean dead(Node n) { return n.isDead(); } - @Override protected int id(Node n) { return n._nid; } - @Override protected int nIns(Node n) { return n.nIns(); } - @Override protected Node in(Node n, int idx) { return n.in(idx); } - @Override protected int nOuts(Node n) { return n.nOuts(); } - @Override protected Node out(Node n, int idx) { return n.out(idx); } - @Override protected int nDeps(Node n) { return n.nDeps(); } - @Override protected Node dep(Node n, int idx) { return n.dep(idx); } - - private ArrayList edges(Node n) { - var edges = new ArrayList(); - String[] names = n instanceof ScopeNode scope && n.nIns() != 0 ? scope.reverseNames() : null; - for( int i = 0; i < n.nIns(); i++ ) { - String name = names == null || i >= names.length ? null : names[i]; - edges.add(new Edge(i, ref(n.in(i)), role(n, i), name)); - } - return edges; - } - - @Override protected GraphSnapshot.Node desc(Node n) { - Projection proj = n instanceof ProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; - String label = n.label(); - return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); - } - - private Kind kind(Node n) { - if( n instanceof StopNode ) return Kind.STOP; - if( n instanceof ScopeNode ) return Kind.SCOPE; - if( n instanceof StartNode ) return Kind.CTRL; - if( n instanceof LoopNode ) return Kind.LOOP; - if( n instanceof RegionNode ) return Kind.REGION; - if( n instanceof PhiNode ) return Kind.PHI; - return n.isCFG() ? Kind.CTRL : isMem(n) ? Kind.MEM : Kind.DATA; - } - - private Role role(Node n, int i) { - if( n instanceof ScopeNode || n instanceof ConstantNode ) return Role.ASSOC; - if( n instanceof PhiNode ) return i == 0 ? Role.ASSOC : isMem(n) ? Role.MEM : Role.DATA; - if( n instanceof ProjNode ) return n.isCFG() ? Role.CTRL : isMem(n) ? Role.MEM : Role.DATA; - if( n instanceof RegionNode && i == 0 ) return Role.ASSOC; - if( i == 0 || n instanceof RegionNode || n instanceof StopNode ) return Role.CTRL; - if( i == 1 && (n instanceof MemOpNode || n instanceof ReturnNode) ) return Role.MEM; - Node def = n.in(i); - if( def != null && isMem(def) ) return Role.MEM; - return Role.DATA; - } - - private boolean isMem(Node n) { return n.isMem() || n._type instanceof TypeMem; } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java deleted file mode 100644 index 1980659e..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.seaofnodes.simple.print; - -import com.seaofnodes.graph.GraphCapture; -import com.seaofnodes.graph.GraphViewer; -import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.node.Node; -import java.util.ArrayList; -import java.io.IOException; - -/** Chapter 10's compilation context; capture and event bookkeeping are shared. */ -public class SimpleGraphObserver extends GraphCapture { - private Parser _parser; - private Node _ret; - private String _phase; - public SimpleGraphObserver() { super(new SimpleGraphAdapter()); } - public static void main(String[] args) throws IOException { - GraphViewer.run(new SimpleGraphObserver()); - } - @Override protected void compile(String src) { - _parser = new Parser(src); - _parser._obs = this; - _phase = "Parse"; - _ret = _parser.parse(); - phase(_phase); - _phase = "Iter"; - com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); - phase(_phase); - _phase = "TypeCheck"; - _parser.STOP.typeCheck(); - } - @Override protected String phase() { return _phase; } - @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } - @Override protected void detach() { - if( _parser != null ) _parser._obs = null; - _parser = null; - _ret = null; - } - @Override protected Node scope() { return _parser._scope; } - @Override protected void roots(ArrayList roots) { - roots.add(Parser.START); - roots.add(_parser.STOP); - roots.addAll(_parser._xScopes); - roots.add(_ret); - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter10/src/main/java/com/seaofnodes/simple/type/Field.java deleted file mode 100644 index 96088f90..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/type/Field.java +++ /dev/null @@ -1,60 +0,0 @@ -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/chapter10/src/main/java/com/seaofnodes/simple/type/Type.java b/chapter10/src/main/java/com/seaofnodes/simple/type/Type.java deleted file mode 100644 index 93e27c76..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/type/Type.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.seaofnodes.simple.type; - -import com.seaofnodes.simple.Utils; - -import java.util.ArrayList; -import java.util.HashMap; - -/** - * These types are part of a Monotone Analysis Framework, - * @see see for example this set of slides. - *

- * The types form a lattice; @see a symmetric complete bounded (ranked) lattice. - *

- * This wild lattice theory will be needed later to allow us to easily beef up - * the analysis and optimization of the Simple compiler... but we don't need it - * now, just know that it is coming along in a later Chapter. - *

g - * One of the fun things here is that while the theory is deep and subtle, the - * actual implementation is darn near trivial and is generally really obvious - * what we're doing with it. Right now, it's just simple integer math to do - * simple constant folding e.g. 1+2 == 3 stuff. - */ - -public class Type { - static final HashMap INTERN = new HashMap<>(); - - // ---------------------------------------------------------- - // Simple types are implemented fully here. "Simple" means: the code and - // type hierarchy are simple, not that the Type is conceptually simple. - static final byte TBOT = 0; // Bottom (ALL) - static final byte TTOP = 1; // Top (ANY) - static final byte TCTRL = 2; // Ctrl flow bottom - static final byte TXCTRL = 3; // Ctrl flow top (mini-lattice: any-xctrl-ctrl-all) - 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; - - public boolean is_simple() { return _type < TSIMPLE; } - private static final String[] STRS = new String[]{"Bot","Top","Ctrl","~Ctrl"}; - protected Type(byte type) { _type = type; } - - public static final Type BOTTOM = new Type( TBOT ).intern(); // ALL - 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; } - - // Strict constant values, things on the lattice centerline. - // Excludes both high and low values - public boolean isConstant() { return false; } - - /** - * 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" - public T intern() { - T nnn = (T)INTERN.get(this); - if( nnn==null ) - INTERN.put(nnn=(T)this,this); - return nnn; - } - - private int _hash; // Hash cache; not-zero when set. - @Override - public final int hashCode() { - if( _hash!=0 ) return _hash; - _hash = hash(); - if( _hash==0 ) _hash = 0xDEADBEEF; // Bad hash from subclass; use some junk thing - return _hash; - } - // Override in subclasses - int hash() { return _type; } - - @Override - public final boolean equals( Object o ) { - if( o==this ) return true; - if( !(o instanceof Type t)) return false; - if( _type != t._type ) return false; - return eq(t); - } - // Overridden in subclasses; subclass can assume "this!=t" and java classes are same - boolean eq(Type t) { return true; } - - - // ---------------------------------------------------------- - public final Type meet(Type t) { - // Shortcut for the self case - if( t == this ) return this; - // Same-type is always safe in the subclasses - if( _type==t._type ) return xmeet(t); - // 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 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()'. - 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; - if( _type==TTOP || t._type==TBOT ) return t; - // 'this' is {TCTRL,TXCTRL} - if( !t.is_simple() ) return BOTTOM; - // 't' is {TCTRL,TXCTRL} - return _type==TCTRL || t._type==TCTRL ? CONTROL : XCONTROL; - } - - public Type dual() { - return switch( _type ) { - case TBOT -> TOP; - case TTOP -> BOTTOM; - case TCTRL -> XCONTROL; - case TXCTRL -> CONTROL; - default -> throw Utils.TODO(); // Should not reach here - }; - } - - // ---------------------------------------------------------- - // 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/chapter10/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/chapter10/src/main/java/com/seaofnodes/simple/type/TypeInteger.java deleted file mode 100644 index 8c77672b..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/type/TypeInteger.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.seaofnodes.simple.type; - -import java.util.ArrayList; - -/** - * Integer Type - */ -public class TypeInteger extends Type { - - public final static TypeInteger TOP = make(false, 0); - public final static TypeInteger BOT = make(false, 1); - public final static TypeInteger ZERO= make(true , 0); - - public final boolean _is_con; - - /** - * The constant value or - * if not constant then 1=bottom, 0=top. - */ - public final long _con; - - private TypeInteger(boolean is_con, long con) { - super(TINT); - _is_con = is_con; - _con = con; - } - public static TypeInteger make(boolean is_con, long con) { - return new TypeInteger(is_con,con).intern(); - } - - 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"); - if( this==BOT ) return sb.append("IntBot"); - 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; } - - @Override - public boolean isConstant() { return _is_con; } - - public long value() { return _con; } - - @Override - public Type xmeet(Type other) { - // Invariant from caller: 'this' != 'other' and same class (TypeInteger) - TypeInteger i = (TypeInteger)other; // Contract - // BOT wins - if ( this==BOT ) return this; - if ( i ==BOT ) return i ; - // TOP loses - if ( i ==TOP ) return this; - if ( this==TOP ) return i ; - // Since both are constants, and are never equals (contract) unequals - // constants fall to bottom - return BOT; - } - - @Override - public Type dual() { - if( isConstant() ) return this; // Constants are a self-dual - 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 - public boolean eq( Type t ) { - TypeInteger i = (TypeInteger)t; // Contract - return _con==i._con && _is_con==i._is_con; - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/chapter10/src/main/java/com/seaofnodes/simple/type/TypeMem.java deleted file mode 100644 index b6fc8824..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/type/TypeMem.java +++ /dev/null @@ -1,66 +0,0 @@ -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) { - if( this==BOT ) return sb.append("MemBot"); - if( this==TOP ) return sb.append("MemTop"); - 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/chapter10/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/chapter10/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java deleted file mode 100644 index 6c874a93..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.seaofnodes.simple.type; - -import java.util.ArrayList; - -/** - * Represents a Pointer to memory. - * - * Null is generic pointer to non-existent memory. - * *void is a non-Null pointer to all possible structs. - * Pointers can be to specific struct types, or a union with Null. - * The distinguished *$BOT ptr represents union of *void and Null. - * The distinguished *$TOP ptr represents the dual of *$BOT. - */ -public class TypeMemPtr extends Type { - // A TypeMemPtr is pair (obj,nil) - // where obj is one of - // (null,TypeStruct). - // where nil is one of - // (true,false) meaning an explicit null is allowed or not - - public final TypeStruct _obj; // null, a TypeStruct, or sentinel TypeStruct.MANY - public final boolean _nil; - - private TypeMemPtr(TypeStruct obj, boolean nil) { - super(TMEMPTR); - _obj = obj; - _nil = nil; - } - public static TypeMemPtr make(TypeStruct obj, boolean nil) { return new TypeMemPtr(obj, nil).intern(); } - public static TypeMemPtr make(TypeStruct obj) { return TypeMemPtr.make(obj, false); } - - public static final TypeMemPtr BOT = make(TypeStruct.BOT,true); - public static final TypeMemPtr TOP = BOT.dual(); - public static final TypeMemPtr NULLPTR = make(TypeStruct.TOP,true); - public static final TypeMemPtr VOIDPTR = NULLPTR.dual(); // A bottom mix of not-null ptrs, like C's void* but not null - public static final TypeMemPtr TEST= make(TypeStruct.TEST,false); - public static void gather(ArrayList ts) { ts.add(NULLPTR); ts.add(BOT); ts.add(TEST); } - - @Override - Type xmeet(Type t) { - TypeMemPtr that = (TypeMemPtr) t; - return TypeMemPtr.make((TypeStruct)_obj.meet(that._obj), _nil | that._nil); - } - - @Override - public TypeMemPtr dual() { return TypeMemPtr.make(_obj==null ? null : _obj.dual(), !_nil); } - - @Override - public Type glb() { - if( _obj==null ) return BOT; - return make(_obj.glb(),true); - } - @Override public TypeMemPtr makeInit() { return NULLPTR; } - - @Override - int hash() { return (_obj==null ? 0xDEADBEEF : _obj.hashCode()) ^ (_nil ? 1024 : 0); } - - @Override - boolean eq(Type t) { - TypeMemPtr ptr = (TypeMemPtr)t; // Invariant - return _obj == ptr._obj && _nil == ptr._nil; - } - - // [void,name,MANY]*[,?] - @Override - public StringBuilder print(StringBuilder sb) { - if( this==BOT ) return sb.append("PtrBot"); - if( this==TOP ) return sb.append("PtrTop"); - if( this== NULLPTR) return sb.append("null"); - if( this== VOIDPTR) return sb.append("*void"); - return _obj.print(sb.append("*")).append(_nil ? "?" : ""); - } - - @Override public String str() { - if( this==BOT ) return "PtrBot"; - if( this==TOP ) return "PtrTop"; - if( this== NULLPTR) return "null"; - if( this== VOIDPTR) return "*void"; - return "*"+_obj.str()+(_nil ? "?" : ""); - } - -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/chapter10/src/main/java/com/seaofnodes/simple/type/TypeStruct.java deleted file mode 100644 index 44741def..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/type/TypeStruct.java +++ /dev/null @@ -1,127 +0,0 @@ -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) { - if( this==BOT ) return sb.append("StructBot"); - if( this==TOP ) return sb.append("StructTop"); - sb.append(_name).append(" {\n"); - for( Field f : _fields ) { - sb.append(" ").append(f._fname).append(":"); - f._type.print(sb); - sb.append(";\n"); - } - return sb.append("}"); - } - - @Override public String str() { - if( this==BOT ) return "StructBot"; - if( this==TOP ) return "StructTop"; - return _name; - } -} diff --git a/chapter10/src/main/java/com/seaofnodes/simple/type/TypeTuple.java b/chapter10/src/main/java/com/seaofnodes/simple/type/TypeTuple.java deleted file mode 100644 index 0aa019a7..00000000 --- a/chapter10/src/main/java/com/seaofnodes/simple/type/TypeTuple.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.seaofnodes.simple.type; - -import java.util.ArrayList; - -public class TypeTuple extends Type { - - public final Type[] _types; - - 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 - Type xmeet(Type other) { - TypeTuple tt = (TypeTuple)other; // contract from xmeet - assert _types.length == tt._types.length; - Type[] ts = new Type[_types.length]; - for( int i=0; i<_types.length; i++ ) - ts[i] = _types[i].meet(tt._types[i]); - return make(ts); - } - - @Override public Type dual() { - Type[] ts = new Type[_types.length]; - for( int i=0; i<_types.length; i++ ) - ts[i] = _types[i].dual(); - 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("["); - for( Type t : _types ) - t.print(sb).append(","); - sb.setLength(sb.length()-1); - sb.append("]"); - 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}); - public static final TypeTuple IF_FALSE = make(new Type[]{Type.XCONTROL,Type. CONTROL}); - - @Override - int hash() { - int sum = 0; - for( Type type : _types ) sum ^= type.hashCode(); - return sum; - } - - @Override - boolean eq( Type t ) { - TypeTuple tt = (TypeTuple)t; // Contract - if( _types.length != tt._types.length ) return false; - for( int i=0; i<_types.length; i++ ) - if( _types[i]!=tt._types[i] ) - return false; - return true; - } - - -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/chapter10/src/test/java/com/seaofnodes/simple/Chapter01Test.java deleted file mode 100644 index 89882b2f..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/Chapter01Test.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.TypeInteger; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class Chapter01Test { - - @Test - public void testSimpleProgram() { - Parser parser = new Parser("return 1;"); - StopNode stop = parser.parse(); - StartNode start = Parser.START; - ReturnNode ret = (ReturnNode)stop.in(0); - - assertTrue(ret.ctrl() instanceof ProjNode); - Node expr = ret.expr(); - if( expr instanceof ConstantNode con ) { - assertEquals(start,con.in(0)); - assertEquals(TypeInteger.constant(1), con._type); - } else { - fail(); - } - } - - @Test - public void testZero() { - Parser parser = new Parser("return 0;"); - parser.parse(); - StartNode start = Parser.START; - for( Node use : start._outputs ) - if( use instanceof ConstantNode con && con._type instanceof TypeInteger ) - assertEquals(TypeInteger.constant(0),con._type); - } - - @Test - public void testBad1() { - try { - new Parser("ret").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Undefined name 'ret'",e.getMessage()); - } - } - - @Test - public void testBad2() { - try { - new Parser("return 0123;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Syntax error: integer values cannot start with '0'",e.getMessage()); - } - } - - @Test - public void testNotBad3() { - // this test used to fail in chapter 1 - assertEquals("return 12;", new Parser("return --12;").parse().print()); - } - - @Test - public void testBad4() { - try { - new Parser("return 100").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Syntax error, expected ;: ",e.getMessage()); - } - } - - @Test - public void testNotBad5() { - // this test used to fail in chapter 1 - assertEquals("return -100;", new Parser("return -100;").parse().print()); - } - - @Test - public void testBad6() { - try { - new Parser("return100").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Undefined name 'return100'",e.getMessage()); - } - } - - @Test - public void testBad7() { - try { - new Parser("return 1;}").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Syntax error, unexpected }",e.getMessage()); - } - } - -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/Chapter02Test.java b/chapter10/src/test/java/com/seaofnodes/simple/Chapter02Test.java deleted file mode 100644 index a6075b75..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/Chapter02Test.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.TypeInteger; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class Chapter02Test { - - @Test - public void testParseGrammar() { - Parser parser = new Parser("return 1+2*3+-5;"); - StopNode ret = parser.parse(); - assertEquals("return 2;", ret.print()); - } - - @Test - public void testAddPeephole() { - Parser parser = new Parser("return 1+2;"); - StopNode ret = parser.parse(); - assertEquals("return 3;", ret.print()); - } - - @Test - public void testSubPeephole() { - Parser parser = new Parser("return 1-2;"); - StopNode ret = parser.parse(); - assertEquals("return -1;", ret.print()); - } - - @Test - public void testMulPeephole() { - Parser parser = new Parser("return 2*3;"); - StopNode ret = parser.parse(); - assertEquals("return 6;", ret.print()); - } - - @Test - public void testDivPeephole() { - Parser parser = new Parser("return 6/3;"); - StopNode ret = parser.parse(); - assertEquals("return 2;", ret.print()); - } - - @Test - public void testMinusPeephole() { - Parser parser = new Parser("return 6/-3;"); - StopNode ret = parser.parse(); - assertEquals("return -2;", ret.print()); - } - - @Test - public void testExample() { - Parser parser = new Parser("return 1+2*3+-5;"); - StopNode ret = parser.parse(); - assertEquals("return 2;", ret.print()); - } - -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/Chapter03Test.java b/chapter10/src/test/java/com/seaofnodes/simple/Chapter03Test.java deleted file mode 100644 index 609dff76..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/Chapter03Test.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.TypeInteger; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class Chapter03Test { - - @Test - public void testVarDecl() { - Parser parser = new Parser("int a=1; return a;"); - StopNode ret = parser.parse(); - assertEquals("return 1;", ret.print()); - } - - @Test - public void testVarAdd() { - Parser parser = new Parser("int a=1; int b=2; return a+b;"); - StopNode ret = parser.parse(); - assertEquals("return 3;", ret.print()); - } - - @Test - public void testVarScope() { - Parser parser = new Parser("int a=1; int b=2; int c=0; { int b=3; c=a+b; } return c;"); - StopNode ret = parser.parse(); - assertEquals("return 4;", ret.print()); - } - - @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); "); - StopNode ret = parser.parse(); - assertEquals("return 8;", ret.print()); - } - - @Test - public void testSelfAssign() { - try { - new Parser("int a=a; return a;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Undefined name 'a'",e.getMessage()); - } - } - - @Test - public void testRedeclareVar() { - try { - new Parser("int a=1; int b=2; int c=0; int b=3; c=a+b;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Redefining name 'b'",e.getMessage()); - } - } - - @Test - public void testBad1() { - try { - new Parser("int a=1; int b=2; int c=0; { int b=3; c=a+b;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Syntax error, expected }: ",e.getMessage()); - } - } - -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/chapter10/src/test/java/com/seaofnodes/simple/Chapter04Test.java deleted file mode 100644 index f1603cc4..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/Chapter04Test.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.TypeInteger; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class Chapter04Test { - - @Test - public void testPeephole() { - Parser parser = new Parser("return 1+arg+2; "); - StopNode ret = parser.parse(); - assertEquals("return (arg+3);", ret.print()); - } - - @Test - public void testPeephole2() { - Parser parser = new Parser("return (1+arg)+2;"); - StopNode ret = parser.parse(); - assertEquals("return (arg+3);", ret.print()); - } - - @Test - public void testAdd0() { - Parser parser = new Parser("return 0+arg;"); - StopNode ret = parser.parse(); - assertEquals("return arg;", ret.print()); - } - - @Test - public void testAddAddMul() { - Parser parser = new Parser("return arg+0+arg;"); - StopNode ret = parser.parse(); - assertEquals("return (arg*2);", ret.print()); - } - - @Test - public void testPeephole3() { - Parser parser = new Parser("return 1+arg+2+arg+3; "); - StopNode ret = parser.parse(); - assertEquals("return ((arg*2)+6);", ret.print()); - } - - @Test - public void testMul1() { - Parser parser = new Parser("return 1*arg;"); - StopNode ret = parser.parse(); - assertEquals("return arg;", ret.print()); - } - - @Test - public void testSub0() { - Parser parser = new Parser("return arg-0;"); - StopNode ret = parser.parse(); - assertEquals("return arg;", ret.print()); - } - - @Test - public void test0Sub() { - Parser parser = new Parser("return 0-arg;"); - StopNode ret = parser.parse(); - assertEquals("return (-arg);", ret.print()); - } - - @Test - public void testSubNeg() { - Parser parser = new Parser("return 3--arg;"); - StopNode ret = parser.parse(); - assertEquals("return (arg+3);", ret.print()); - } - - @Test - public void testNegSub() { - Parser parser = new Parser("return -arg-3;"); - StopNode ret = parser.parse(); - assertEquals("return (-(arg+3));", ret.print()); - } - - @Test - public void testVarArg() { - Parser parser = new Parser("return arg; "); - StopNode stop = parser.parse(); - ReturnNode ret = stop.ret(); - assertTrue(ret.in(0) instanceof ProjNode); - assertTrue(ret.in(1) instanceof ProjNode); - } - - @Test - public void testConstantArg() { - Parser parser = new Parser("return arg; ", TypeInteger.constant(2)); - StopNode ret = parser.parse(); - assertEquals("return 2;", ret.print()); - } - - @Test - public void testCompEq() { - Parser parser = new Parser("return 3==3; "); - StopNode ret = parser.parse(); - assertEquals("return 1;", ret.print()); - } - - @Test - public void testCompEq2() { - Parser parser = new Parser("return 3==4; "); - StopNode ret = parser.parse(); - assertEquals("return 0;", ret.print()); - } - - @Test - public void testCompNEq() { - Parser parser = new Parser("return 3!=3; "); - StopNode ret = parser.parse(); - assertEquals("return 0;", ret.print()); - } - - @Test - public void testCompNEq2() { - Parser parser = new Parser("return 3!=4; "); - StopNode ret = parser.parse(); - assertEquals("return 1;", ret.print()); - } - - @Test - public void testBug1() { - Parser parser = new Parser("int a=arg+1; int b=a; b=1; return a+2; "); - StopNode ret = parser.parse(); - assertEquals("return (arg+3);", ret.print()); - } - - @Test - public void testBug2() { - Parser parser = new Parser("int a=arg+1; a=a; return a; "); - StopNode ret = parser.parse(); - assertEquals("return (arg+1);", ret.print()); - } - - @Test - public void testBug3() { - try { - new Parser("inta=1; return a;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Undefined name 'inta'",e.getMessage()); - } - } - - @Test - public void testBug4() { - Parser parser = new Parser("return -arg;"); - StopNode ret = parser.parse(); - assertEquals("return (-arg);", ret.print()); - } - - @Test - public void testBug5() { - Parser parser = new Parser("return arg--2;"); - StopNode ret = parser.parse(); - assertEquals("return (arg--2);", ret.print()); - } -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/chapter10/src/test/java/com/seaofnodes/simple/Chapter05Test.java deleted file mode 100644 index 35e54fa9..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/Chapter05Test.java +++ /dev/null @@ -1,213 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.TypeInteger; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class Chapter05Test { - - @Test - public void testIfStmt() { - Parser parser = new Parser( -""" -int a = 1; -if (arg == 1) - a = arg+2; -else { - a = arg-3; - -} - -return a;"""); - StopNode ret = parser.parse().iterate(); - assertEquals("return Phi(Region18,(arg+2),(arg-3));", ret.toString()); - } - - @Test - public void testTest() { - Parser parser = new Parser( -""" -int c = 3; -int b = 2; -if (arg == 1) { - b = 3; - c = 4; -} -return c;""", TypeInteger.BOT); - StopNode ret = parser.parse().iterate(); - assertEquals("return Phi(Region16,4,3);", ret.toString()); - } - - @Test - public void testReturn2() { - Parser parser = new Parser( -""" -if( arg==1 ) - return 3; -else - return 4; -""", TypeInteger.BOT); - StopNode stop = parser.parse(); - assertEquals("Stop[ return 3; return 4; ]", stop.toString()); - } - - @Test - public void testIfMergeB() { - Parser parser = new Parser( -""" -int a=arg+1; -int b=0; -if( arg==1 ) - b=a; -else - b=a+1; -return a+b;"""); - StopNode ret = parser.parse().iterate(); - assertEquals("return ((arg*2)+Phi(Region21,2,3));", ret.toString()); - } - - @Test - public void testIfMerge2() { - Parser parser = new Parser( -""" -int a=arg+1; -int b=arg+2; -if( arg==1 ) - b=b+a; -else - a=b+1; -return a+b;"""); - StopNode ret = parser.parse().iterate(); - assertEquals("return ((Phi(Region32,(arg*2),arg)+arg)+Phi(Region,4,5));", ret.toString()); - } - - @Test - public void testMerge3() { - Parser parser = new Parser( -""" -int a=1; -if( arg==1 ) - if( arg==2 ) - a=2; - else - a=3; -else if( arg==3 ) - a=4; -else - a=5; -return a; -""", TypeInteger.BOT); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region36,Phi(Region22,2,3),Phi(Region34,4,5));", stop.toString()); - } - - @Test - public void testMerge4() { - Parser parser = new Parser( -""" -int a=0; -int b=0; -if( arg ) - a=1; -if( arg==0 ) - b=2; -return arg+a+b; -""", TypeInteger.BOT); - StopNode stop = parser.parse(); - assertEquals("return ((arg+Phi(Region13,1,0))+Phi(Region28,2,0));", stop.toString()); - } - - @Test - public void testMerge5() { - Parser parser = new Parser( -""" -int a=arg==2; -if( arg==1 ) -{ - a=arg==3; -} -return a;"""); - StopNode ret = parser.parse().iterate(); - assertEquals("return (arg==Phi(Region16,3,2));", ret.toString()); - } - - @Test - public void testTrue() { - StopNode stop = new Parser("return true;").parse(); - assertEquals("return 1;",stop.toString()); - } - - @Test - public void testHalfDef() { - try { - new Parser("if( arg==1 ) int b=2; return b;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Cannot define a new name on one arm of an if",e.getMessage()); - } - } - - @Test - public void testHalfDef2() { - try { - new Parser("if( arg==1 ) { int b=2; } else { int b=3; } return b;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Undefined name 'b'",e.getMessage()); - } - } - - @Test - public void testRegress1() { - try { - new Parser("if(arg==2) int a=1; else int b=2; return a;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Cannot define a new name on one arm of an if",e.getMessage()); - } - } - - - @Test - public void testBadNum() { - try { - new Parser("return 1-;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Syntax error, expected an identifier or expression: ;",e.getMessage()); - } - } - - @Test - public void testKeyword1() { - try { - new Parser("int true=0; return true;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Expected an identifier, found 'true'",e.getMessage()); - } - } - - @Test - public void testKeyword2() { - try { - new Parser("int else=arg; if(else) else=2; else else=1; return else;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Expected an identifier, found 'else'",e.getMessage()); - } - } - - @Test - public void testKeyword3() { - try { - new Parser("int a=1; ififif(arg)inta=2;return a;").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("Undefined name 'ififif'",e.getMessage()); - } - } - -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/Chapter06Test.java b/chapter10/src/test/java/com/seaofnodes/simple/Chapter06Test.java deleted file mode 100644 index 097c2617..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/Chapter06Test.java +++ /dev/null @@ -1,294 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.TypeInteger; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class Chapter06Test { - - @Test - public void testPeepholeReturn() { - Parser parser = new Parser( -""" -if( true ) return 2; -return 1; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return 2;", stop.toString()); - assertTrue(stop.ret().ctrl() instanceof ProjNode); - } - - @Test - public void testPeepholeRotate() { - Parser parser = new Parser( -""" -int a = 1; -if (arg) - a = 2; -return (arg < a) < 3; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return ((arg 4 ) - break; -} -return arg; - """); - StopNode stop = parser.parse().iterate(); - assertEquals("return arg;", stop.toString()); - } - - @Test - public void testRegress2() { - Parser parser = new Parser("if(1) return 0; else while(arg>--arg) arg=arg+1; return 0;"); - StopNode stop = parser.parse().iterate(); - assertEquals("return 0;", stop.toString()); - } - - @Test - public void testBreakOutsideLoop() { - try { - new Parser(""" -if(arg <= 10) { - break; - arg = arg + 1; -} -return arg; -""").parse(); - fail(); - } catch( RuntimeException e ) { - assertEquals("No active loop for a break or continue",e.getMessage()); - } - } - - @Test - public void testRegress3() { - Parser parser = new Parser(""" -while(arg < 10) { - break; -} -return arg; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return arg;", stop.toString()); - } - - @Test - public void testRegress4() { - Parser parser = new Parser(""" -int a = 1; -while(arg < 10) { - a = a + 1; - if (arg > 2) { - int a = 17; - break; - } -} -return a; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region28,Phi(Loop7,1,(Phi_a+1)),Add);", stop.toString()); - assertTrue(stop.ret().ctrl() instanceof RegionNode); - } - - @Test - public void testRegress5() { - Parser parser = new Parser(""" -int a = 1; -while(1) { - a = a + 1; - if (a<10) continue; - break; -} -return a; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return (Phi(Loop7,1,Add)+1);", stop.toString()); - assertTrue(stop.ret().ctrl() instanceof ProjNode prj && prj._idx==1 ); - } - -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/chapter10/src/test/java/com/seaofnodes/simple/Chapter09Test.java deleted file mode 100644 index 048a58c7..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/Chapter09Test.java +++ /dev/null @@ -1,301 +0,0 @@ -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; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.Ignore; - -import static org.junit.Assert.*; - -public class Chapter09Test { - - @Test - @Ignore - public void testJig() { - Parser parser = new Parser(""" -int v0=0; -arg=0; -while(v0) { - while(1) if(arg*arg*0==0) {} - while(0) {} - arg=1; -} -return 0; - """); - StopNode stop = parser.parse().iterate(); - } - - @Test - public void testGVN1() { - Parser parser = new Parser( - """ -int x = arg + arg; -if(arg < 10) { - return arg + arg; -} -else { - x = x + 1; -} -return x; - """); - StopNode stop = parser.parse(); - assertEquals("Stop[ return (arg*2); return (Mul+1); ]", stop.toString()); - Assert.assertEquals(2L, Evaluator.evaluate(stop, 1)); - Assert.assertEquals(23L, Evaluator.evaluate(stop, 11)); - } - - @Test - public void testGVN2() { - Parser parser = new Parser( - """ -return arg*arg-arg*arg; - """); - StopNode stop = parser.parse(); - assertEquals("return 0;", stop.toString()); - Assert.assertEquals(0L, Evaluator.evaluate(stop, 1)); - } - - @Test - public void testWorklist1() { - Parser parser = new Parser( - """ -int step = 1; -while (arg < 10) { - arg = arg + step + 1; -} -return arg; - """); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop7,arg,(Phi_arg+2));", stop.toString()); - Assert.assertEquals(11L, Evaluator.evaluate(stop, 1)); - } - - @Test - public void testWorklist2() { - Parser parser = new Parser( - """ -int cond = 0; -int one = 1; -while (arg < 10) { - if (cond) one = 2; - arg = arg + one*3 + 1; -} -return arg; - """); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop8,arg,(Phi_arg+4));", stop.toString()); - Assert.assertEquals(13L, Evaluator.evaluate(stop, 1)); - } - - @Test - public void testWorklist3() { - Parser parser = new Parser( - """ -int v1 = 0; -int v2 = 0; -int v3 = 0; -int v4 = 0; -int v5 = 0; -int v6 = 0; -int v7 = 0; -int v8 = 0; -while (arg) { - if (v1) v2 = 1; - if (v2) v3 = 1; - if (v3) v4 = 1; - if (v4) v5 = 1; - if (v5) v6 = 1; - if (v6) v7 = 1; - if (v7) v8 = 1; - arg = arg + v8 + 1; -} -return arg; - """); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop14,arg,(Phi_arg+1));", stop.toString()); - } - - @Test - public void testRegionPeepBug() { - Parser parser = new Parser( - """ -int v0=0; -int v1=0; -while(v1+arg) { - arg=0; - int v2=v0; - while(arg+1) {} - v0=1; - v1=v2; -} - """); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - @Test - public void testWhile0() { - Parser parser = new Parser("while(0) continue; if(0) arg=0;"); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - @Test - public void testWhile1() { - Parser parser = new Parser(""" -if(0) while(0) { - int arg=arg; - while(0) {} -} -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - - @Test - public void testPrecedence() { - Parser parser = new Parser("return 3-1+2;"); - StopNode stop = parser.parse().iterate(); - assertEquals("return 4;", stop.toString()); - } - - @Test - public void testSwap2() { - Parser parser = new Parser("return 1+(1+1);"); - StopNode stop = parser.parse().iterate(); - assertEquals("return 3;", stop.toString()); - } - - - @Test - public void testFuzz0() { - Parser parser = new Parser(""" -int one = 1; -int a = 0; -int zero = 0; -while(arg) { - a = -(one + a + 2); - arg = arg + 1; - one = one + zero; -} -return a; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop9,0,(-(Phi_a+3)));", stop.toString()); - } - - @Test - public void testFuzz1() { - Parser parser = new Parser(""" -while(1) {} -while(arg) break; -while(arg) arg=0; -arg=0; -int v0=0!=0<-0; -return -0+0+0; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - - @Test - public void testFuzz2() { - Parser parser = new Parser("return 0+-0;"); - StopNode stop = parser.parse().iterate(); - assertEquals("return 0;", stop.toString()); - } - - @Test - public void testFuzz3() { - Parser parser = new Parser("int v0=0; while(0==69) while(v0) return 0;"); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - @Test - public void testFuzz4() { - Parser parser = new Parser(""" -while(1) { - arg=0<=0; - if(1<0) while(arg==-0) arg=arg-arg; -} -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - @Test - public void testFuzz5() { - Parser parser = new Parser(""" -{ - int v0=0; - while(1) - int v1=0--0; - while(v0) - break; - while(-v0) { - while(0+0+v0) continue; - break; - } - if(-0!=-0+0+v0) while(0+0+0+0) - break; -} -return 0!=0; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - @Test - public void testFuzz6() { - Parser parser = new Parser(""" -int v0=0; -while(0==1) while(v0) - v0=1+v0; - """); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - @Test - public void testFuzz7() { - Parser parser = new Parser(""" -while(1) {} -int v0=0; -while(v0) - {} -int v1=0; -while(1) - v1=1; -return v1+v0; - """); - StopNode stop = parser.parse().iterate(); - assertEquals("Stop[ ]", stop.toString()); - } - - @Test - public void testFuzz8() { - Parser parser = new Parser("while(arg) arg = arg - 1; return arg;"); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop6,arg,(Phi_arg-1));", stop.toString()); - } - - @Test - public void testMeet() { - Type t1 = Type.TOP; - Type t2 = TypeInteger.TOP; - Assert.assertEquals(TypeInteger.TOP, t1.meet(t2)); - Assert.assertEquals(TypeInteger.TOP, t2.meet(t1)); - t1 = Type.BOTTOM; - t2 = TypeInteger.BOT; - Assert.assertEquals(Type.BOTTOM, t1.meet(t2)); - Assert.assertEquals(Type.BOTTOM, t2.meet(t1)); - } - -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/chapter10/src/test/java/com/seaofnodes/simple/Chapter10Test.java deleted file mode 100644 index da9a4c5f..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/Chapter10Test.java +++ /dev/null @@ -1,461 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.node.Node; -import com.seaofnodes.simple.node.StopNode; -import com.seaofnodes.simple.type.*; -import org.junit.Test; - -import java.util.Arrays; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class Chapter10Test { - @Test public void testDeadReferenceReturn() { - for( String body : new String[] { - "return new S; return 0;", - "if(1) return new S; else return 0;" - } ) { - String src = "struct S { int x; }; " + body; - var stop = new Parser(src).parse().iterate(); - org.junit.Assert.assertTrue(body, - stop.ret().expr()._type instanceof com.seaofnodes.simple.type.TypeMemPtr); - } - } - - - - // Issue #246: null-check guards start in Chapter 10; arrays arrive in Chapter 15. - private static final String NULLABLE_POINT_SOURCE = """ - struct Point { int x; }; - Point point = new Point; - point.x = 42; - Point? p = null; - if (arg) p = point; - """; - - @Test - public void testNullGuards() { - for( String body : new String[] { - "if (p != null) return p.x; return -1;", - "if (null != p) return p.x; return -1;", - "if (!!!!p) return p.x; return -1;", - "if (!!!p) return -1; else return p.x;", - "int b = !!p; if (b) return p.x + b - 1; return -1;" - } ) { - StopNode stop = new Parser(NULLABLE_POINT_SOURCE+body).parse().iterate(); - assertEquals(body,-1L,com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,0)); - assertEquals(body,42L,com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,1)); - } - } - - @Test - public void testNullGuardErrors() { - for( String body : new String[] { - "return p.x;", - "if (p == null) return p.x; return -1;", - "if (!!point) return p.x; return -1;", - "if (!!p) { int x = p.x; } return p.x;" - } ) { - try { - new Parser(NULLABLE_POINT_SOURCE+body).parse().iterate(); - fail(body); - } catch( RuntimeException e ) { - // Known null is rejected during parsing in these chapters. - String expected = body.startsWith("if (p == null)") - ? "Accessing unknown field 'x' from 'null'" : "Might be null accessing 'x'"; - assertEquals(body,expected,e.getMessage()); - } - } - } - - @Test - public void testFuzzer() { - Parser parser = new Parser( -""" -int a = arg/3; -int b = arg*5; -int x = arg*7; -int y = arg/11; -int p; int g; int h; -if( (arg/13)==0 ) { - p = x + y; - g = x; - h = y; -} else { - p = a + b; - g = a; - h = b; -} -int r = g+h; -return p-r; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return 0;", stop.toString()); - } - - @Test - public void testStruct() { - Parser parser = new Parser(""" -struct Bar { - int a; - int b; -} -struct Foo { - int x; -} -Foo? foo = null; -Bar bar = new Bar; -bar.a = 1; -bar.a = 2; -return bar.a; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return 2;", stop.toString()); - } - - @Test - public void testExample() { - Parser parser = new Parser(""" -struct Vector2D { int x; int y; } -Vector2D v = new Vector2D; -v.x = 1; -if (arg) - v.y = 2; -else - v.y = 3; -return v; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return new Vector2D;", stop.toString()); - } - - @Test - public void testBug() { - Parser parser = new Parser(""" -struct s0 { - int v0; -} -s0? v1=null; -int v3=v1.zAicm; -"""); - try { parser.parse(); fail(); } - catch( Exception e ) { assertEquals("Accessing unknown field 'zAicm' from 'null'",e.getMessage()); } - } - - @Test - public void testBug2() { - Parser parser = new Parser(""" -struct s0 { int v0; } -arg=0+new s0.0; -"""); - try { parser.parse(); fail(); } - catch( Exception e ) { assertEquals("Expected an identifier, found 'null'",e.getMessage()); } - } - - @Test - public void testLoop() { - Parser parser = new Parser(""" -struct Bar { int a; } -Bar bar = new Bar; -while (arg) { - bar.a = bar.a + 2; - arg = arg + 1; -} -return bar.a; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Loop10,0,(Phi_a+2));", stop.toString()); - } - - @Test - public void testIf() { - Parser parser = new Parser(""" -struct Bar { int a; } -Bar bar = new Bar; -if (arg) bar = null; -bar.a = 1; -return bar.a; -"""); - try { parser.parse().iterate(); fail(); } - catch( Exception e ) { assertEquals("Type null is not of declared type *Bar",e.getMessage()); } - } - - @Test - public void testIf2() { - Parser parser = new Parser(""" -struct Bar { int a; } -Bar? bar = null; -if (arg) bar = new Bar; -bar.a = 1; -return bar.a; -"""); - try { parser.parse().iterate(); fail(); } - catch( Exception e ) { assertEquals("Might be null accessing 'a'",e.getMessage()); } - } - - @Test - public void testIf3() { - Parser parser = new Parser(""" -struct Bar { int a; } -Bar bar = null; -if (arg) bar = null; -bar.a = 1; -return bar.a; -"""); - try { parser.parse(); fail(); } - catch( Exception e ) { assertEquals("Type null is not of declared type *Bar", e.getMessage()); } - } - - @Test - public void testIfOrNull() { - Parser parser = new Parser(""" -struct Bar { int a; } -Bar? bar = new Bar; -if (arg) bar = null; -if( bar ) bar.a = 1; -return bar; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region15,null,new Bar);", stop.toString()); - } - - @Test - public void testIfOrNull2() { - Parser parser = new Parser( -""" -struct Bar { int a; } -Bar? bar = new Bar; -if (arg) bar = null; -int rez = 3; -if( !bar ) rez=4; -else bar.a = 1; -return rez; -"""); - StopNode stop = parser.parse().iterate(); - assertEquals("return Phi(Region32,4,3);", stop.toString()); - } - - @Test - public void testWhileWithNullInside() { - Parser parser = new Parser(""" -struct s0 {int v0;} -s0? v0 = new s0; -int ret = 0; -while(arg) { - ret = v0.v0; - v0 = null; - arg = arg - 1; -} -return ret; -"""); - try { parser.parse().iterate(); fail(); } - catch( Exception e ) { assertEquals("Might be null accessing 'v0'", e.getMessage()); } - } - - @Test - public void testRedeclareStruct() { - Parser parser = new Parser(""" -struct s0 { - int v0; -} -s0? v1=new s0; -s0? v1; -v1=new s0; -"""); - try { parser.parse(); fail(); } - catch( Exception e ) { assertEquals("Redefining name 'v1'", e.getMessage()); } - } - - @Test - public void testIter() { - // Build and use an iterator - Parser parser = new Parser( -""" -struct Iter { - int x; - int len; -} -Iter i = new Iter; -i.len = arg; -int sum=0; -while( i.x < i.len ) { - sum = sum + i.x; - i.x = i.x + 1; -} -return sum; -"""); - StopNode stop = parser.parse().iterate(); - 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/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java deleted file mode 100644 index 7dcb88e5..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.seaofnodes.simple.fuzzer; - -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; - -/** - * Implementation of the fuzzer. - * Random testcases are generated using the ScriptGenerator. - * This will generate a script similar to how the parser parses it but instead of parsing it will - * randomly choose a variation the parser would parse and generate it. - * These scripts are the parsed by the parser and all exceptions are caught and filtered - * to only show one occurrence of one problem. This includes script which generate different - * results when graphs optimized with different worklist seeds are executed. - * To aid debugging scripts that cause errors are then reduced by applying rules and checking - * that the same issue persists. - */ -public class Fuzzer { - - private static final int EVAL_TIMEOUT = 1000; - - /** - * List of exceptions already encountered. - * This is used to filter new exceptions and discard them if they are already found. - */ - private final ArrayList exceptions = new ArrayList<>(); - - /** - * Filter and record an exception in a script. - * @param e The exception caused by the script - * @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, 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 ========="); - System.out.println(Reducer.reduce(script, e, reproducer)); - System.out.println("==========================="); - 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), seed); - } - } - - /** - * Run one test with the given seed, and check for peephole counts. - * @param seed The seed to use for generating this test case - */ - public int fuzzPeepTiming(long seed, int max_nids) { - var rand = new Random(seed); - var sb = new StringBuilder(); - new ScriptGenerator(rand, sb, true).genProgram(); - try { - var parser = new Parser(sb.toString()); - - var stop = parser.parse(); - int parse_peeps= Node.ITER_CNT; - int parse_nops = Node.ITER_NOP_CNT; - double parse_nop_ratio = (double)parse_nops/parse_peeps; - int nids = Node.UID(); - if( nids <= max_nids ) return max_nids; - - double parse_peeps_per_node = (double)parse_peeps/nids; - - stop.iterate(); - int iter_peeps= Node.ITER_CNT; - int iter_nops = Node.ITER_NOP_CNT; - double iter_nop_ratio = (double)iter_nops/iter_peeps; - double iter_peeps_per_node = (double)iter_peeps/nids; - - System.out.printf("%6d | Parsing: peeps: %5d, nops: %5d, ratio: %5.3f, last UID: %5d ps/node: %5.3f | Iter: peeps: %6d, nops: %6d, ratio: %5.3f, ps/node: %5.3f\n", - seed, - parse_peeps,parse_nops,parse_nop_ratio,nids,parse_peeps_per_node, - iter_peeps , iter_nops, iter_nop_ratio, iter_peeps_per_node); - return nids; - } catch (Throwable e) { - // ignore exceptions, let the normal fullPeeps catch and reduce, - // we're interested in valid program optimizations - } - return max_nids; - } - - - /** - * Run one test with the given seed. - * @param seed The seed to use for generating this test case - */ - public void fuzzPeeps(long seed) { - var rand = new Random(seed); - var sb = new StringBuilder(); - var valid = new ScriptGenerator(rand, sb, true).genProgram(); - check(sb.toString(), valid, seed); - } - - - /** - * Check that no exceptions happened. - * @return true if no exception happened - */ - public boolean noExceptions() { - return exceptions.isEmpty(); - } - - /** Run a fixed chapter-local seed without reducing or swallowing failures. */ - public void fuzzPeepsRegression(long seed) { - var rand = new Random(seed); - var sb = new StringBuilder(); - var valid = new ScriptGenerator(rand, sb, true).genProgram(); - try { - runCheck(sb.toString(), valid); - } catch (Throwable e) { - AssertionError ae = new AssertionError("Fuzzer regression seed failed: " + seed); - ae.initCause(e); - throw ae; - } - } -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java deleted file mode 100644 index f947a234..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.seaofnodes.simple.fuzzer; - -import com.seaofnodes.simple.IterPeeps; -import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.node.Node; -import com.seaofnodes.simple.node.StopNode; - -import java.io.OutputStream; -import java.io.PrintStream; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.reflect.Field; -import java.util.BitSet; - -/** - * Some utilities for the fuzzer. - */ -class FuzzerUtils { - - /** - * Get the val of a private field - */ - @SuppressWarnings("unchecked") - public static T getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { - return (T) (obj instanceof Class clz ? getField(clz, name).get(null) : getField(obj.getClass(), name).get(obj)); - } - - /** - * Get access to a private field - */ - public static Field getField(Class clz, String name) throws NoSuchFieldException { - var field = clz.getDeclaredField(name); - try { - field.setAccessible(true); - } catch (Throwable e) { - // Intentionally left empty - } - return field; - } - - /** - * Try to rethrow an exception or create a runtime exception and throw it. - */ - public static RuntimeException rethrow(Throwable e) { - if (e instanceof RuntimeException re) return re; - if (e instanceof Error er) throw er; - return new RuntimeException(e); - } - - /** - * Copy of Node.WVISIST - * Used to clear as exceptions might happen in the walk and left this bitset not cleared - */ - private static final BitSet NodeWalkVisit; - private static final java.util.Random WorkRandom; - - /** - * Write access to Iterate.MID_ASSERT - * Exception could happen mid assert and left this at true. Used to reset to false prior to parsing. - */ - private static final MethodHandle set_MID_ASSERT; - - /** - * Some problems might trigger debug output. Suppress this output with a null print stream during fuzzing. - */ - public static final PrintStream NULL_PRINT_STREAM = new PrintStream(OutputStream.nullOutputStream()); - - static { - try { - NodeWalkVisit = getFieldValue(Node.class, "WVISIT"); - WorkRandom = getFieldValue(getFieldValue(IterPeeps.class, "WORK"), "_R"); - set_MID_ASSERT = MethodHandles.lookup().unreflectSetter(getField( IterPeeps.class, "MID_ASSERT")); - } catch (Exception e) { - throw new ExceptionInInitializerError(e); - } - } - - /** - * Check that the messages are similar. - */ - 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); - } - - /** - * Check if both stack traces have the same first entry. Empty stack traces can happen with NPE optimization - * and allow the other to be any stack trace. - */ - private static boolean sameStackTraceOrigin(StackTraceElement[] a, StackTraceElement[] b) { - if (a == null || a.length == 0 || b == null || b.length == 0) return true; // One or both stack traces are missing. - return a[0].equals(b[0]); - } - - /** - * Checks if two exceptions are caused by the same problem. - */ - 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 (!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 - if (!isExceptionFromSameCause(a.getCause(), b.getCause())) return false; // Different causes - var o1 = a.getSuppressed(); - var o2 = b.getSuppressed(); - if (o1 == null || o2 == null || o1.length != o2.length) return o1 == o2; // Different suppression - for (int i=0; i {} - - /** - * Holder for the rewrite rules - */ - 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("-"), ""), - new Replace(Pattern.compile("\\b\\+-"), "-"), - new Replace(Pattern.compile("(?=])(?:-|\\*|/|>=|<=|>|<|!=|==)(?![+\\-*/<>=])"), "+"), - 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"), - 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+ \\{[^{}]*}"), "") - }; - - /** - * Predicate to test if the reduced script generates the same error. - */ - private final Predicate tester; - - /** - * Create a reducer with a test function. - * @param tester Test function which returns true when the same issue is found in the script. - */ - private Reducer(Predicate tester) { - this.tester = tester; - } - - /** - * Rewrites the script with one rewrite rule. - */ - private String doReplacement(String script, Pattern pattern, Replacer[] replacements) { - var matcher = pattern.matcher(script); - var sb = new StringBuilder(); - 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(); - 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); - } - last = matcher.start(); - skip = 1; - } - sb.append(script, last, script.length()); - return sb.toString(); - } - - /** - * Clean up variable names from the random ones generated by the fuzzer. - */ - private String cleanVariables(String script) { - var matcher = Pattern.compile("\\b([0-9a-zA-Z_]+) ([0-9a-zA-Z_]+)").matcher(script); - int[] num = {0, 0}; - while (matcher.find()) { - 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; - } - } - return script; - } - - /** - * Run all rewrite rules once. - */ - private String doAllReplacements(String script) { - for (var replace: REPLACEMENTS) { - String old; - do { - old = script; - script = doReplacement(script, replace.pattern, replace.replacements); - } while (replace.iterate && !old.equals(script)); - } - return script; - } - - /** - * Reduce the script to a minimal version which produces the same problem with a test function. - * @param tester Test function which returns true when the same issue is found in the script. - */ - public static String reduce(String script, Predicate tester) { - var reducer = new Reducer(tester); - String old; - do { - old = script; - script = reducer.doAllReplacements(script); - script = reducer.cleanVariables(script); - } while (!script.equals(old)); - return script; - } - - /** - * Reduce the script to a minimal version which produces the same problem with a reproducer. - * @param ex The exception originally caused by the script - * @param reproducer Test function which throws an exception which will be checked against ex. - */ - public static String reduce(String script, Throwable ex, Consumer reproducer) { - return reduce(script, s->{ - Throwable nex = null; - try { - reproducer.accept(s); - } catch (Throwable e) { - nex = e; - } - return FuzzerUtils.isExceptionFromSameCause(nex, ex); - }); - } - -} diff --git a/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java deleted file mode 100644 index 155063ab..00000000 --- a/chapter10/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java +++ /dev/null @@ -1,792 +0,0 @@ -package com.seaofnodes.simple.fuzzer; - -import com.seaofnodes.simple.Parser; -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. - * This generator will generate the same script for two random number generators initialized with the same seed. - * To generate valid code it implements the parser but instead of parsing it emits the statements and expressions. - * It is guaranteed to terminate. - */ -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 - */ - private static final int INDENTATION = 4; - /** - * Valid characters for identifiers. The last 10 are not valid for the first character. - */ - private static final String VAR_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; - /** - * List of keywords not valid for identifiers. - */ - private static final HashSet KEYWORDS; - - static { - try { - KEYWORDS = FuzzerUtils.getFieldValue(new Parser(""), "KEYWORDS"); - } catch (Throwable e) { - throw new ExceptionInInitializerError(e); - } - } - - /** - * Flag for a statement that it does not pass on control flow to the next statement. - */ - private static final int FLAG_STOP = 0x01; - /** - * Flag for a statement that it contains a final if statement without an else branch. - */ - private static final int FLAG_IF_WITHOUT_ELSE = 0x02; - /** - * Binary operators. - */ - private static final String[] BINARY_OP = {"==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/"}; - /** - * Unary operators. - */ - 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. - */ - private final Random random; - /** - * Indentation depth. - */ - private int indentation = 0; - /** - * Output string builder - */ - private final StringBuilder sb; - /** - * Number of nested loops - */ - private int loopDepth = 0; - /** - * Current scope start in the variables list. - */ - private int currScopeStart = 0; - /** - * The depth of blocks allowed. - */ - private int depth = MAX_BLOCK_DEPTH; - /** - * The depth of expressions allowed. - */ - private int exprDepth = MAX_EXPRESSION_DEPTH; - /** - * Current variables in scope. - */ - 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 boolean generateValid; - /** - * If this script might be invalid. - */ - private boolean maybeInvalid = false; - - - /** - * - * @param random the random number generator. - * @param sb the script will be written to this string builder. - * @param generateValid if the script should be guaranteed valid or if it is allowed to contain bugs. - */ - public ScriptGenerator(Random random, StringBuilder sb, boolean generateValid) { - this.random = random; - this.sb = sb; - this.generateValid = generateValid; - } - - /** - * Random number with log distribution - * @param max The maximum value - * @return A random number in the range [0, max) with higher numbers being more uncommon. - */ - private int randLog(int max) { - max--; - int n = 1<=0; i--) { - if ((num & (1<2) return false; - maybeInvalid = true; - return true; - } - - /** - * Lookup a variable by name - * @param name name of the variable - * @return The index of the variable in variables or -1 - */ - private int lookup(String name) { - for (int i=variables.size()-1; i>=0; i--) { - if (variables.get(i).name.equals(name)) return i; - } - return -1; - } - - /** - * Lookup a variable by name - * @param name name of the variable - * @return THe variable of null if not found - */ - private Variable lookupVar(String name) { - int idx = lookup(name); - return idx == -1 ? null : variables.get(idx); - } - - /** - * Add a variable to the current scope - * @param name The name of the variable - * @param type The declared type of the variable - */ - private void addVariable(String name, Type type) { - var old = lookupVar(name); - variables.add(new Variable(name, type, old)); - if (old != null) old.shadowed = true; - } - - /** - * Pop variables to level to - * @param to Level until which variables should be popped - */ - private void popVariables(int to) { - while (variables.size() > to) { - var v = variables.removeLast(); - if (v.shadowing != null) v.shadowing.shadowed = false; - } - } - - /** - * Get a random visible variable patching pred - * @param pred The predicate the variable needs to match - * @return A random visible variable matching pred or null if no variable mached pred. - */ - private Variable findVisibleVariablesMatching(Predicate pred) { - int num=0; - for (var v:variables) { - if (!v.shadowed && pred.test(v)) num++; - } - if (num == 0) return null; - num = random.nextInt(num); - for (var v:variables) { - if (!v.shadowed && pred.test(v) && num-- == 0) return v; - } - throw new AssertionError(); - } - - /** - * Get a structure by name - * @param name Name of the structure - * @return The structure of null - */ - private TypeStruct getStruct(String name) { - for (var s:structs) { - if (s.name.equals(name)) return s; - } - return null; - } - - /** - * Generate a variable name. This might be new random name or a name from an outer scope. - * If the program is allowed to be invalid this can return keywords and already used variable names in the current scope. - * @return The variable name. - */ - private String getVarName() { - if (currScopeStart > 0 && random.nextInt(10) > 7) { - // Use a variable outside the current scope. - var v = variables.get(random.nextInt(currScopeStart)); - if (lookup(v.name) < currScopeStart) return v.name; - if (!generateValid) { - maybeInvalid = true; - return v.name; - } - } - // Generate a new random variable name - StringBuilder sb = getRandomName(); - var v = sb.toString(); - if (!generateValid && (KEYWORDS.contains(v) || lookup(v) >= currScopeStart)) { - maybeInvalid = true; - return v; - } - 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; - addVariable(ScopeNode.ARG0, TYPE_INT); - currScopeStart = variables.size(); - if ((genStatements() & FLAG_STOP) == 0) { - if (generateValid || random.nextInt(10)<7) { - genReturn(); - } else { - this.maybeInvalid = true; - } - } - generateValid = oldGenerateValid; - return !this.maybeInvalid; - } - - /** - * Generate a list of statements. - * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the last statement generated - */ - public int genStatements() { - 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 -> 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; - var ret = genBlock(); - indentation += INDENTATION; - depth--; - yield ret; - } - case 6 -> genIf(); - case 7 -> genCountedLoop(); - case 8 -> genWhile(); - case 9 -> genAssignment(); - case 10 -> genNullCheck(); - default -> genExit(); - }; - depth++; - } - indentation -= INDENTATION; - allowStructs = oldAllowStructs; - return res; - } - - /** - * Generate an exit. This can be return or break and continue when in a loop. - * @return FLAG_STOP - */ - public int genExit() { - if (loopDepth == 0 && generateValid) return genReturn(); - return switch (random.nextInt(7)) { - case 0,1,2 -> { - if (loopDepth == 0) maybeInvalid = true; - sb.append("continue;"); - yield FLAG_STOP; - } - case 3,4,5 -> { - if (loopDepth == 0) maybeInvalid = true; - sb.append("break;"); - yield FLAG_STOP; - } - default -> genReturn(); - }; - } - - /** - * Generate a block statement. After a certain depth only empty blocks are generated to ensure termination of the generator. - * @return flag FLAG_STOP for the generated statement - */ - public int genBlock() { - if (depth == 0) { - sb.append("{}"); - return 0; - } - var oldAllowStructs = allowStructs; - var oldCSS = currScopeStart; - allowStructs = false; - currScopeStart = variables.size(); - depth--; - sb.append("{\n"); - indentation += INDENTATION; - var stop = genStatements(); - indentation -= INDENTATION; - printIndentation().append("}"); - popVariables(currScopeStart); - depth++; - currScopeStart = oldCSS; - allowStructs = oldAllowStructs; - return stop & ~FLAG_IF_WITHOUT_ELSE; - } - - /** - * Generate an if statement with an optional else block. - * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement - */ - public int genIf() { - sb.append("if("); - genExpression(TYPE_INT); - sb.append(") "); - var stop = genStatementBlock(); - if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { - sb.append("\n"); - printIndentation().append("else "); - stop &= genStatementBlock(); - } else { - stop = FLAG_IF_WITHOUT_ELSE; - } - 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(TYPE_INT); - sb.append(") "); - loopDepth++; - genStatementBlock(); - loopDepth--; - 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() { - 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(";"); - addVariable(name, type); - return 0; - } - - /** - * Generate an assignment statement. - * @return 0 - */ - public int genAssignment() { - 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(declared); - sb.append(";"); - return 0; - } - - /** - * Generate a return statement. - * @return FLAG_STOP - */ - public int genReturn() { - var type = getType(); - sb.append("return "); - genExpression(type); - sb.append(";"); - return FLAG_STOP; - } - - /** - * Generate a binary expression. - * This method does not care about operator precedence. - */ - 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(TYPE_INT); - sb.append(BINARY_OP[random.nextInt(BINARY_OP.length)]); - } - genUnary(TYPE_INT); - } - - /** - * Generate a unary expression. - */ - 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)]); - } - 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(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 constant. - */ - 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(Type type) { - if (generateInvalid()) { - sb.append(getRandomName()); - return; - } - 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); - } - -} diff --git a/chapter10a/Makefile b/chapter10a/Makefile new file mode 100644 index 00000000..5ed21084 --- /dev/null +++ b/chapter10a/Makefile @@ -0,0 +1,7 @@ +# Standalone compiler snapshot; shared rules also work in linearized checkouts. +include $(firstword $(wildcard ../build-support/early-chapter.mk build-support/early-chapter.mk)) +test_cp += com.seaofnodes.simple.FuzzerWrap + +.PHONY: fuzzer +fuzzer: $(test_classes) + java -ea -cp "$(CLZDIR)/main$(SEP)$(CLZDIR)/test$(SEP)$(jars)" org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap diff --git a/chapter10a/README.md b/chapter10a/README.md new file mode 100644 index 00000000..30db40d3 --- /dev/null +++ b/chapter10a/README.md @@ -0,0 +1,273 @@ +# Chapter 10a: Structs and Memory + +[Previous: Chapter 9](../chapter09/README.md) | +[Next: Chapter 10b](../chapter10b/README.md) + +This chapter adds user-defined structs, pointers, allocation, field loads and +stores, and null checking. It also introduces an evaluator that executes the +graph, including its memory effects. The [grammar](docs/10-grammar.md) is the +same in 10a and 10b; the next chapter improves the memory representation. + +## One memory value + +Arithmetic nodes depend on the values they use. A load also depends on when it +observes memory. In this chapter every store consumes the preceding memory +state and produces the next one. A load consumes memory without changing it. +This makes the source program's effect ordering explicit in the graph. + +```java +struct Point { int x; int y; } +Point p = new Point; +p.x = 3; +p.y = arg; +return p.x; +``` + +`NewNode` produces the pointer. The parser emits stores of zero for both fields, +then stores 3 into `x` and `arg` into `y`. The final load observes the memory +after the `y` store. We deliberately keep that dependency here, even though +different fields are independent. Chapter 10b will remove it. + +![One chain orders stores and loads](docs/example1.svg) + +These diagrams are schematic dependency graphs: arrows run from definitions to +uses. Pointer and value inputs are omitted where they do not explain ordering. + +| Node | Inputs | Result | +|---|---|---| +| `New` | Control and a struct type | A fresh, non-null object pointer | +| `Store` | Memory, pointer, field name, value | The updated whole-memory state | +| `Load` | Memory, pointer, field name | The value of the field | +| `Start` | External entry | Control, initial memory, and `arg` | +| `Return` | Control, result, memory | Completion with all preceding effects retained | + +The memory edge is a dependency token, not a copy of the heap. In the evaluator, +objects contain mutable field arrays; executing a Store changes an array entry. +The graph determines the legal execution order. + +## Memory is an SSA variable + +The parser keeps one hidden binding named `$mem`. Source identifiers cannot +contain `$`, so user code cannot name it. Reading and updating memory uses the +same `ScopeNode` lookup and update operations as a local variable. Start has a +fixed tuple: control at index 0, memory at index 1, and `arg` at index 2. + +For an `if`, the two branches can finish with different memory values. An +ordinary Phi selects the memory from the branch that executed: + +![Whole memory across a branch](docs/example2a.svg) + +Loops reuse Chapter 8's lazy Phi construction. Reading or updating `$mem` in a +loop creates an initially incomplete memory Phi. Closing the loop supplies its +backedge. `break`, `continue`, nested loops, and early returns use the same scope +mechanics as scalar variables. + +![Whole memory around a loop](docs/example2b.svg) + +Every Return consumes the current whole-memory value. This keeps stores alive +even when the returned scalar does not depend on them, or when the program +returns the modified object itself. + +## Local memory optimizations + +A load immediately following a store to the **same pointer and field** can use +the stored value. A store immediately following another store to the same +address can discard the earlier store when no other user needs it. Both the +pointer and field checks matter: two different fields of one object are +different addresses, as are the same fields of two different objects. + +A load can also move through a memory Phi when doing so exposes a useful +load-after-store fold. On a loop, the backedge must fold, preventing this rewrite +from repeatedly moving the load around the cycle. + +The single chain is conservative. A store to `y` can prevent forwarding an +earlier store to `x`, even when the objects are identical. This is a missed +optimization, not a correctness problem; it motivates the next chapter. + +## Enhanced Type Lattice + +The Type Lattice for Simple has a major revision in this chapter. + +![Graph1](./docs/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 the whole-memory token + * `⊤:mem` is `TypeMem.TOP`, the high end of the memory domain. + * `⊥:mem` is `TypeMem.BOT`, our conservative type for the whole-memory state. + * Neither token describes the values stored in individual fields. +* 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`. +* 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()`. + + +## Executing the graph + +The evaluator includes a small late scheduler. A load must execute before a +later store that consumes the same memory definition, even though there is no +ordinary data edge from the load to that store. These load/store +anti-dependencies supplement the explicit graph edges. + +![A load must precede a clobbering store](docs/example2c.svg) + +The scheduler is sufficient to execute this chapter. Chapter 11 develops +global code motion and scheduling as a separate topic. + +## Build and test + +From this directory, with JDK 21 or newer, Make, and a shell: + +```sh +make lib +make tests +make release +make view +``` + +Make builds this snapshot directly, without Maven. `make release` produces +`build/release/simple.jar`; `make view` launches the shared interactive graph +viewer. Maven and IDEA module descriptions are also supplied. Tests include all +earlier chapters, memory ordering, returned object contents, and nested loops. + +Next, [Chapter 10b](../chapter10b/README.md) keeps this one parser binding while +splitting the graph into independent memory slices. diff --git a/chapter10a/chapter10a.iml b/chapter10a/chapter10a.iml new file mode 100644 index 00000000..3f930463 --- /dev/null +++ b/chapter10a/chapter10a.iml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chapter10a/docs/10-grammar.md b/chapter10a/docs/10-grammar.md new file mode 100644 index 00000000..5b9b680e --- /dev/null +++ b/chapter10a/docs/10-grammar.md @@ -0,0 +1,125 @@ +# Grammar for Chapter 10a + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | structDeclaration + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatment + ; + +field + : 'int' IDENTIFIER ';' + ; + +fields + : field+ + ; + +structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + | fieldExpression '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +structName + : IDENTIFIER + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + | structName ('?')? IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression + ; + +newExpression + : 'new' IDENTIFIER + ; + +fieldExpression + : primaryExpresson '.' IDENTIFIER + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | 'null' + | newExpression + | '(' expression ')' + | fieldExpression + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter10a/docs/Makefile b/chapter10a/docs/Makefile new file mode 100644 index 00000000..055882b2 --- /dev/null +++ b/chapter10a/docs/Makefile @@ -0,0 +1,14 @@ +GV := $(wildcard *.gv) +SVG := $(patsubst %.gv,%.svg,$(GV)) +.DELETE_ON_ERROR: + +.PHONY: all +all: $(SVG) + +%.svg: %.gv + dot -Tsvg $< > $@ + @sed -i 's/\r$$//' $@ + +.PHONY: clean +clean: + @rm -f $(SVG) diff --git a/chapter10a/docs/example1.gv b/chapter10a/docs/example1.gv new file mode 100644 index 00000000..38214174 --- /dev/null +++ b/chapter10a/docs/example1.gv @@ -0,0 +1,10 @@ +digraph Memory { + rankdir=LR; + node [shape=box]; + M0 [label="Initial memory"]; + X [label="Store p.x = 3"]; + Y [label="Store p.y = arg"]; + L [label="Load p.x"]; + M0 -> X -> Y -> L [color=blue]; + Y -> Return [color=blue]; L -> Return; +} diff --git a/chapter10a/docs/example1.svg b/chapter10a/docs/example1.svg new file mode 100644 index 00000000..7ae4b560 --- /dev/null +++ b/chapter10a/docs/example1.svg @@ -0,0 +1,73 @@ + + + + + + +Memory + + + +M0 + +Initial memory + + + +X + +Store p.x = 3 + + + +M0->X + + + + + +Y + +Store p.y = arg + + + +X->Y + + + + + +L + +Load p.x + + + +Y->L + + + + + +Return + +Return + + + +Y->Return + + + + + +L->Return + + + + + diff --git a/chapter10a/docs/example2a.gv b/chapter10a/docs/example2a.gv new file mode 100644 index 00000000..13fd75e0 --- /dev/null +++ b/chapter10a/docs/example2a.gv @@ -0,0 +1,10 @@ +digraph Memory { + rankdir=LR; + node [shape=box]; + M [label="Incoming memory"]; + A [label="True: Store x"]; + B [label="False: Store y"]; + P [label="Phi(memory)"]; + M -> A -> P [color=blue]; M -> B -> P [color=blue]; + Region -> P [style=dashed]; P -> Return [color=blue]; +} diff --git a/chapter10a/docs/example2a.svg b/chapter10a/docs/example2a.svg new file mode 100644 index 00000000..1752659e --- /dev/null +++ b/chapter10a/docs/example2a.svg @@ -0,0 +1,85 @@ + + + + + + +Memory + + + +M + +Incoming memory + + + +A + +True: Store x + + + +M->A + + + + + +B + +False: Store y + + + +M->B + + + + + +P + +Phi(memory) + + + +A->P + + + + + +B->P + + + + + +Return + +Return + + + +P->Return + + + + + +Region + +Region + + + +Region->P + + + + + diff --git a/chapter10a/docs/example2b.gv b/chapter10a/docs/example2b.gv new file mode 100644 index 00000000..9db0f5fb --- /dev/null +++ b/chapter10a/docs/example2b.gv @@ -0,0 +1,8 @@ +digraph Memory { + rankdir=LR; + node [shape=box]; + P [label="Phi(memory)"]; + S [label="Loop-body Store"]; + Entry -> P [color=blue]; P -> S -> P [color=blue]; + Loop -> P [style=dashed]; P -> Return [color=blue]; +} diff --git a/chapter10a/docs/example2b.svg b/chapter10a/docs/example2b.svg new file mode 100644 index 00000000..1f106d63 --- /dev/null +++ b/chapter10a/docs/example2b.svg @@ -0,0 +1,73 @@ + + + + + + +Memory + + + +P + +Phi(memory) + + + +S + +Loop-body Store + + + +P->S + + + + + +Return + +Return + + + +P->Return + + + + + +S->P + + + + + +Entry + +Entry + + + +Entry->P + + + + + +Loop + +Loop + + + +Loop->P + + + + + diff --git a/chapter10a/docs/example2c.gv b/chapter10a/docs/example2c.gv new file mode 100644 index 00000000..0a9f0462 --- /dev/null +++ b/chapter10a/docs/example2c.gv @@ -0,0 +1,9 @@ +digraph Memory { + rankdir=LR; + node [shape=box]; + M [label="Prior memory"]; + L [label="Load p.x"]; + S [label="Store p.x"]; + M -> L [color=blue]; M -> S [color=blue]; + L -> S [style=dashed,label="execute load first"]; +} diff --git a/chapter10a/docs/example2c.svg b/chapter10a/docs/example2c.svg new file mode 100644 index 00000000..90f90920 --- /dev/null +++ b/chapter10a/docs/example2c.svg @@ -0,0 +1,50 @@ + + + + + + +Memory + + + +M + +Prior memory + + + +L + +Load p.x + + + +M->L + + + + + +S + +Store p.x + + + +M->S + + + + + +L->S + + +execute load first + + + diff --git a/chapter10a/docs/lattice.gv b/chapter10a/docs/lattice.gv new file mode 100644 index 00000000..b2d8255e --- /dev/null +++ b/chapter10a/docs/lattice.gv @@ -0,0 +1,151 @@ +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]; + + TOP -> MEM_TOP; + MEM_TOP -> MEM_BOT; + MEM_BOT -> BOT; + + + // ---------------------- + // 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;} + {rank=same; INT_BOT; CTRL; STRUCT_BOT; MEM_BOT; TUPLE_BOT} + {rank=same; BOT;} +} diff --git a/chapter10a/docs/lattice.svg b/chapter10a/docs/lattice.svg new file mode 100644 index 00000000..aa296366 --- /dev/null +++ b/chapter10a/docs/lattice.svg @@ -0,0 +1,466 @@ + + + + + + +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_BOT + +⊥ +:mem + + + +MEM_TOP->MEM_BOT + + + + + +MEM_BOT->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/chapter10a/pom.xml b/chapter10a/pom.xml new file mode 100644 index 00000000..141ff7d6 --- /dev/null +++ b/chapter10a/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + com.seaofnodes + simple + 1.0 + + chapter10a + jar + Chapter 10a + + ${project.basedir}/../graph + + diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/IRPrinter.java b/chapter10a/src/main/java/com/seaofnodes/simple/IRPrinter.java new file mode 100644 index 00000000..1ac32b1a --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/IRPrinter.java @@ -0,0 +1,188 @@ +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 { + + // Print a node on 1 line, columnar aligned, as: + // NNID NNAME DDEF DDEF [[ UUSE UUSE ]] TYPE + // 1234 sssss 1234 1234 1234 1234 1234 1234 tttttt + public static void _printLine( Node n, StringBuilder sb ) { + sb.append("%4d %-7.7s ".formatted(n._nid,n.label())); + if( n.isDead() ) { + sb.append("DEAD\n"); + return; + } + for( Node def : n._inputs ) + sb.append(def==null ? "____ " : "%4d ".formatted(def._nid)); + for( int i = n._inputs.size(); i<3; i++ ) + sb.append(" "); + sb.append(" [[ "); + for( Node use : n._outputs ) + sb.append(use==null ? "____ " : "%4d ".formatted(use._nid)); + int lim = 5 - Math.max(n._inputs.size(),3); + for( int i = n._outputs.size(); i 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 + ArrayList rpos = new ArrayList<>(); + BitSet visit = new BitSet(); + for( int i=bfs._lim; i< bfs._bfs.size(); i++ ) + postOrd( bfs._bfs.get(i), rpos, visit, bfs._bs); + // Reverse the post-order walk + StringBuilder sb = new StringBuilder(); + boolean gap=false; + for( int i=rpos.size()-1; i>=0; i-- ) { + Node n = rpos.get(i); + if( n.isCFG() || n.isMultiHead() ) { + if( !gap ) sb.append("\n"); // Blank before multihead + printLine( n, sb, llvmFormat ); // Print head + while( --i >= 0 ) { + Node t = rpos.get(i); + if( !t.isMultiTail() ) { i++; break; } + printLine( t, sb, llvmFormat ); + } + sb.append("\n"); // Blank after multitail + gap = true; + } else { + printLine( n, sb, llvmFormat ); + gap = false; + } + } + return sb.toString(); + } + + private static void postOrd(Node n, ArrayList rpos, BitSet visit, BitSet bfs) { + if( !bfs.get(n._nid) ) + return; // Not in the BFS visit + if( visit.get(n._nid) ) return; // Already post-order walked + visit.set(n._nid); + // First walk the CFG, then everything + if( n.isCFG() ) { + for( Node use : n._outputs ) + if( use!=null && use.isCFG() && use.nOuts()>=1 && !(use._outputs.get(0) instanceof LoopNode) ) + postOrd(use, rpos,visit,bfs); + for( Node use : n._outputs ) + if( use!=null && use.isCFG() ) + postOrd(use,rpos,visit,bfs); + } + for( Node use : n._outputs ) + if( use != null ) + postOrd(use, rpos,visit,bfs); + // Post-order + rpos.add(n); + } + + // Breadth-first search, broken out in a class to keep in more independent. + // Maintains a root-set of Nodes at the limit (or past by 1 if MultiHead). + public static class BFS { + // A breadth first search, plus MultiHeads for any MultiTails + public final ArrayList _bfs; + public final BitSet _bs; // Visited members by node id + public final int _depth; // Depth limit + public final int _lim; // From here to _bfs._len can be roots for a reverse search + public BFS( Node base, int d ) { + _depth = d; + _bfs = new ArrayList<>(); + _bs = new BitSet(); + + add(base); // Prime the pump + int idx=0, lim=1; // Limit is where depth counter changes + while( idx < _bfs.size() ) { // Ran out of nodes below depth + Node n = _bfs.get(idx++); + for( Node def : n._inputs ) + if( def!=null && !_bs.get(def._nid) ) + add(def); + if( idx==lim ) { // Depth counter changes at limit + if( --d < 0 ) + break; // Ran out of depth + lim = _bfs.size(); // New depth limit + } + } + // Toss things past the limit except multi-heads + while( idx < _bfs.size() ) { + Node n = _bfs.get(idx); + if( n.isMultiHead() ) idx++; + else del(idx); + } + // Root set is any node with no inputs in the visited set + lim = _bfs.size(); + for( int i=_bfs.size()-1; i>=0; i-- ) + if( !any_visited(_bfs.get(i)) ) + swap( i,--lim); + _lim = lim; + } + void swap( int x, int y ) { + if( x==y ) return; + Node tx = _bfs.get(x); + Node ty = _bfs.get(y); + _bfs.set(x,ty); + _bfs.set(y,tx); + } + void add(Node n) { + _bfs.add(n); + _bs.set(n._nid); + } + void del(int idx) { + Node n = Utils.del(_bfs, idx); + _bs.clear(n._nid); + } + boolean any_visited( Node n ) { + for( Node def : n._inputs ) + if( def!=null && _bs.get(def._nid) ) + return true; + return false; + } + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/IterPeeps.java b/chapter10a/src/main/java/com/seaofnodes/simple/IterPeeps.java new file mode 100644 index 00000000..7075d526 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/IterPeeps.java @@ -0,0 +1,208 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Random; + +/** + * The IterOptim runs after parsing. It iterates the peepholes to a fixed point + * so that no more peepholes apply. This should be linear because peepholes rarely + * (never?) increase code size. The graph should monotonically reduce in some + * dimension, which is usually size. It might also reduce in e.g. number of + * MulNodes or Load/Store nodes, swapping out more "expensive" Nodes for cheaper + * ones. + * + * The theoretical overall worklist is mindless just grabbing the next thing and + * doing it. If the graph changes, put the neighbors on the worklist. Lather, + * Rinse, Repeat until the worklist runs dry. + * + * The main issues we have to deal with: + * + *
    + *
  • Nodes have uses; replacing some set of Nodes with another requires more graph + * reworking. Not rocket science, but it can be fiddly. Its helpful to have a + * small set of graph munging utilities, and the strong invariant that the graph + * is stable and correct between peepholes. In our case `Node.subsume` does + * most of the munging, building on our prior stable Node utilities.
  • + * + *
  • Changing a Node also changes the graph "neighborhood". The neigbors need to + * be checked to see if THEY can also peephole, and so on. After any peephole + * or graph update we put a Nodes uses and defs on the worklist.
  • + * + *
  • Our strong invariant is that for all Nodes, either they are on the worklist + * OR no peephole applies. This invariant is easy to check, although expensive. + * Basically the normal "iterate peepholes to a fixed point" is linear, and this + * check is linear at each peephole step... so quadratic overall. Its a useful + * assert, but one we can disable once the overall algorithm is stable - and + * then turn it back on again when some new set of peepholes is misbehaving. + * The code for this is turned on in `IterOptim.iterate` as `assert + * progressOnList(stop);`
  • + *
+ */ +public abstract class IterPeeps { + + private static final WorkList WORK = new WorkList<>(); + + public static N add( N n ) { + return (N)WORK.push(n); + } + + public static void addAll( ArrayList ary ) { + WORK.addAll(ary); + } + + /** + * Iterate peepholes to a fixed point + */ + public static StopNode iterate(StopNode stop) { + assert progressOnList(stop); + int cnt=0; + + Node n; + while( (n=WORK.pop()) != null ) { + if( n.isDead() ) continue; + cnt++; // Useful for debugging, searching which peephole broke things + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + if( obs != null ) obs.before(n); + Node x = n.peepholeOpt(); + if( x != null ) { + if( x.isDead() ) { + if( obs != null ) obs.after(n, x, true); + continue; + } + // peepholeOpt can return brand-new nodes, needing an initial type set + if( x._type==null ) x.setType(x.compute()); + // Changes require neighbors onto the worklist + if( x != n || !(x instanceof ConstantNode) ) { + // All outputs of n (changing node) not x (prior existing node). + for( Node z : n._outputs ) WORK.push(z); + // Everybody gets a free "go again" in case they didn't get + // made in their final form. + WORK.push(x); + // If the result is not self, revisit all inputs (because + // there's a new user), and replace in the graph. + if( x != n ) { + for( Node z : n. _inputs ) WORK.push(z); + n.subsume(x); + } + } + // If there are distant neighbors, move to worklist + n.moveDepsToWorklist(); + assert progressOnList(stop); // Very expensive assert + } + if( n.isUnused() && !(n instanceof StopNode) ) + n.kill(); // Just plain dead + if( obs != null ) obs.after(n, x, true); + } + + return stop; + } + + // Visit ALL nodes and confirm the invariant: + // Either you are on the WORK worklist OR running `iter()` makes no progress. + + // This invariant ensures that no progress is missed, i.e., when the + // worklist is empty we have indeed done all that can be done. To help + // with debugging, the {@code assert} is broken out in a place where it is easy to + // stop if a change is found. + + // Also, the normal usage of `iter()` may attempt peepholes with distance + // neighbors and these should fail, but will then try to add dependencies + // {@link #Node.addDep} which is a side effect in an assert. The {@link + // #midAssert} is used to stop this side effect. + private static boolean MID_ASSERT; + public static boolean midAssert() { return MID_ASSERT; } + private static boolean progressOnList(Node stop) { + MID_ASSERT = true; + int old_cnt = Node.ITER_CNT, old_nop = Node.ITER_NOP_CNT; + Node changed = stop.walk( n -> { + if( WORK.on(n) ) return null; + Node m = n.peepholeOpt(); + if( m==null ) return null; + System.err.println("BREAK HERE FOR BUG"); + return m; + }); + Node.ITER_CNT = old_cnt; Node.ITER_NOP_CNT = old_nop; + MID_ASSERT = false; + return changed==null; + } + + public static void reset() { + WORK.clear(); + } + + /** + * Classic WorkList, with a fast add/remove, dup removal, random pull. + * The Node's nid is used to check membership in the worklist. + */ + public static class WorkList { + + private Node[] _es; + private int _len; + private final BitSet _on; // Bit set if Node._nid is on WorkList + private final Random _R; // For randomizing pull from the WorkList + private final long _seed; + + /* Useful stat - how many nodes are processed in the post parse iterative opt */ + private long _totalWork = 0; + + WorkList() { this(123); } + WorkList(long seed) { + _es = new Node[1]; + _len=0; + _on = new BitSet(); + _seed = seed; + _R = new Random(); + _R.setSeed(_seed); + } + + /** + * Pushes a Node on the WorkList, ensuring no duplicates + * If Node is null it will not be added. + */ + public E push( E x ) { + if( x==null ) return null; + int idx = x._nid; + if( !_on.get(idx) ) { + _on.set(idx); + if( _len==_es.length ) + _es = Arrays.copyOf(_es,_len<<1); + _es[_len++] = x; + _totalWork++; + } + return x; + } + + public void addAll( ArrayList ary ) { + for( E n : ary ) + push(n); + } + + /** + * True if Node is on the WorkList + */ + boolean on( E x ) { return _on.get(x._nid); } + + /** + * Removes a random Node from the WorkList; null if WorkList is empty + */ + E pop() { + if( _len == 0 ) return null; + int idx = _R.nextInt(_len); + E x = (E)_es[idx]; + _es[idx] = _es[--_len]; // Compress array + _on.clear(x._nid); + return x; + } + + public void clear() { + _len = 0; + _on.clear(); + _R.setSeed(_seed); + _totalWork = 0; + } + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/Parser.java b/chapter10a/src/main/java/com/seaofnodes/simple/Parser.java new file mode 100644 index 00000000..7734f4df --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/Parser.java @@ -0,0 +1,827 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.graph.GraphObserver; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +import java.util.*; + +/** + * The Parser converts a Simple source program to the Sea of Nodes intermediate + * representation directly in one pass. There is no intermediate Abstract + * Syntax Tree structure. + *

+ * This is a simple recursive descent parser. All lexical analysis is done here as well. + */ +public class Parser { + + // Current compilation context; the observer belongs to the parser. + public static Parser PARSER; + public GraphObserver _obs; + public int pos() { return _lexer._position; } + + + /** + * A Global Static, unique to each compilation. This is a public, so we + * can make constants everywhere without having to thread the StartNode + * through the entire parser and optimizer. + *

+ * To make the compiler multithreaded, this field will have to move into a TLS. + */ + public static StartNode START; + + public StopNode STOP; + + // The Lexer. Thin wrapper over a byte[] buffer with a cursor. + private final Lexer _lexer; + + /** + * Current ScopeNode - ScopeNodes change as we parse code, but at any point of time + * there is one current ScopeNode. The reason the current ScopeNode can change is to do with how + * we handle branching. See {@link #parseIf()}. + *

+ * Each ScopeNode contains a stack of lexical scopes, each scope is a symbol table that binds + * variable names to Nodes. The top of this stack represents current scope. + *

+ * We keep a list of all ScopeNodes so that we can show them in graphs. + * @see #parseIf() + * @see #_xScopes + */ + public ScopeNode _scope; + + /** + * List of keywords disallowed as identifiers + */ + 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"); + }}; + + + /** + * We clone ScopeNodes when control flows branch; it is useful to have + * a list of all active ScopeNodes for purposes of visualization of the SoN graph + */ + public final Stack _xScopes = new Stack<>(); + + ScopeNode _continueScope; + ScopeNode _breakScope; + + // Mapping from a Struct name to a Struct. + public static Map OBJS = new HashMap<>(); + + public Parser(String source, TypeInteger arg) { + PARSER = this; + Node.reset(); + IterPeeps.reset(); + OBJS.clear(); + _lexer = new Lexer(source); + _scope = new ScopeNode(); + _continueScope = _breakScope = null; + START = new StartNode(new Type[]{ Type.CONTROL, TypeMem.BOT, arg }); + STOP = new StopNode(source); + } + + public Parser(String source) { + this(source, TypeInteger.BOT); + } + + @Override + public String toString() { return _lexer.toString(); } + + String src() { return new String( _lexer._input ); } + + // Debugging utility to find a Node by index + public static Node find(int nid) { return START.find(nid); } + + private Node ctrl() { return _scope.ctrl(); } + + private Node ctrl(Node n) { return _scope.ctrl(n); } + + public StopNode parse() { + _xScopes.push(_scope); + // Enter a new scope for the initial control and arguments + _scope.push(); + _scope.define(ScopeNode.CTRL, Type.CONTROL , new ProjNode(START, 0, ScopeNode.CTRL).peephole()); + _scope.define(ScopeNode.ARG0, TypeInteger.BOT, new ProjNode(START, 2, ScopeNode.ARG0).peephole()); + _scope.define("$mem", TypeMem.BOT, new ProjNode(START, 1, "$mem").peephole()); + parseBlock(); + _scope.pop(); + _xScopes.pop(); + if (!_lexer.isEOF()) throw error("Syntax error, unexpected " + _lexer.getAnyNextToken()); + STOP.peephole(); + return STOP; + } + + /** + * Parses a block + * + *

+     *     '{' statements '}'
+     * 
+ * Does not parse the opening or closing '{}' + * @return a {@link Node} or {@code null} + */ + private Node parseBlock() { + // Enter a new scope + _scope.push(); + while (!peek('}') && !_lexer.isEOF()) + parseStatement(); + // Exit scope + _scope.pop(); + return null; + } + + /** + * Parses a statement + * + *
+     *     returnStatement | declStatement | blockStatement | ifStatement | expressionStatement
+     * 
+ * @return a {@link Node} or {@code null} + */ + private Node parseStatement() { + 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(";") ) 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 + return parseStatement(); + } + + /** + * Parses a while statement + * + *
+     *     while ( expression ) statement
+     * 
+ * @return a {@link Node}, never {@code null} + */ + private Node parseWhile() { + + var savedContinueScope = _continueScope; + var savedBreakScope = _breakScope; + + require("("); + + // Loop region has two control inputs, the first is the entry + // point, and second is back edge that is set after loop is parsed + // (see end_loop() call below). Note that the absence of back edge is + // used as an indicator to switch off peepholes of the region and + // associated phis; see {@code inProgress()}. + + ctrl(new LoopNode(ctrl()).peephole()); // Note we set back edge to null here + + // At loop head, we clone the current Scope (this includes all + // names in every nesting level within the Scope). + // We create phis eagerly for all the names we find, see dup(). + + // Save the current scope as the loop head + ScopeNode head = _scope.keep(); + // Clone the head Scope to create a new Scope for the body. + // Create phis eagerly as part of cloning + _xScopes.push(_scope = _scope.dup(true)); // The true argument triggers creating phis + + // Parse predicate + var pred = require(parseExpression(), ")"); + // IfNode takes current control and predicate + Node ifNode = new IfNode(ctrl(), pred).peephole(); + // Setup projection nodes + Node ifT = new ProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + Node ifF = new ProjNode(ifNode.unkeep(), 1, "False").peephole(); + + // Clone the body Scope to create the break/exit Scope which accounts for any + // side effects in the predicate. The break/exit Scope will be the final + // scope after the loop, and its control input is the False branch of + // the loop predicate. Note that body Scope is still our current scope. + ctrl(ifF); + _xScopes.push(_breakScope = _scope.dup()); + + // No continues yet + _continueScope = null; + + // Parse the true side, which corresponds to loop body + // Our current scope is the body Scope + ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection + parseStatement(); // Parse loop body + + // Merge the loop bottom into other continue statements + if (_continueScope != null) { + _continueScope = jumpTo(_continueScope); + _scope.kill(); + _scope = _continueScope; + } + + // The true branch loops back, so whatever is current _scope.ctrl gets + // added to head loop as input. endLoop() updates the head scope, and + // goes through all the phis that were created earlier. For each phi, + // it sets the second input to the corresponding input from the back + // edge. If the phi is redundant, it is replaced by its sole input. + var exit = _breakScope; + head.endLoop(_scope, exit); + head.unkeep().kill(); + + _xScopes.pop(); // Cleanup + _xScopes.pop(); // Cleanup + + _continueScope = savedContinueScope; + _breakScope = savedBreakScope; + + // At exit the false control is the current control, and + // the scope is the exit scope after the exit test. + return _scope = exit; + } + + private ScopeNode jumpTo(ScopeNode toScope) { + ScopeNode cur = _scope.dup(); + ctrl(new ConstantNode(Type.XCONTROL).peephole()); // Kill current scope + // Prune nested lexical scopes that have depth > than the loop head + // We use _breakScope as a proxy for the loop head scope to obtain the depth + while( cur._scopes.size() > _breakScope._scopes.size() ) + 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" + if (toScope == null) + return cur; + // toScope is either the break scope, or a scope that was created here + assert toScope._scopes.size() <= _breakScope._scopes.size(); + toScope.ctrl(toScope.mergeScopes(cur)); + return toScope; + } + + private void checkLoopActive() { if (_breakScope == null) throw Parser.error("No active loop for a break or continue"); } + + private Node parseBreak () { checkLoopActive(); return ( _breakScope = require(jumpTo( _breakScope ),";")); } + private Node parseContinue() { checkLoopActive(); return (_continueScope = require(jumpTo( _continueScope ),";")); } + + /** + * Parses a statement + * + *
+     *     if ( expression ) statement [else statement]
+     * 
+ * @return a {@link Node}, never {@code null} + */ + private Node parseIf() { + require("("); + // Parse predicate + var pred = require(parseExpression(), ")").keep(); + // IfNode takes current control and predicate + Node ifNode = new IfNode(ctrl(), pred).peephole(); + // Setup projection nodes + Node ifT = new ProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + Node ifF = new ProjNode(ifNode.unkeep(), 1, "False").peephole().keep(); + // In if true branch, the ifT proj node becomes the ctrl + // But first clone the scope and set it as current + int ndefs = _scope.nIns(); + ScopeNode fScope = _scope.dup(); // Duplicate current scope + fScope.ctrl(ifF); // The pending arm already starts at False + _xScopes.push(fScope); // For graph visualization we need all scopes + + // 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; + + // Parse the false side + _scope = fScope; // Restore scope, then parse else block if any + ifF.unkeep(); // fScope already owns the false control + if (matchx("else")) { + _scope.upcast(ifF,pred,true); // Up-cast predicate + parseStatement(); + fScope = _scope; + } + pred.unkeep(); + + if( tScope.nIns() != ndefs || fScope.nIns() != ndefs ) + throw error("Cannot define a new name on one arm of an if"); + + // Merge results + _scope = tScope; + _xScopes.pop(); // Discard pushed from graph display + + return ctrl(tScope.mergeScopes(fScope)); + } + + + /** + * Parses a return statement; "return" already parsed. + * The $ctrl edge is killed. + * + *
+     *     'return' expr ;
+     * 
+ * @return an expression {@link Node}, never {@code null} + */ + private Node parseReturn() { + var expr = require(parseExpression(), ";"); + Node ret = STOP.addReturn(new ReturnNode(ctrl(), expr, _scope).peephole()); + ctrl(new ConstantNode(Type.XCONTROL).peephole()); // Kill control + return ret; + } + + + + /** + * Parses an expression statement or a declaration statement where type is a struct + * + *
+     *      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() { + 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 + * + *
+     *     type name = expression ';'
+     * 
+ * @return an expression {@link Node} + */ + private Node parseDecl(Type t) { + var name = requireId(); + 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: + * + *
+     *     expr : compareExpr
+     * 
+ * @return an expression {@link Node}, never {@code null} + */ + private Node parseExpression() { return parseComparison(); } + + /** + * Parse an expression of the form: + * + *
+     *     expr : additiveExpr op additiveExpr
+     * 
+ * @return an comparator expression {@link Node}, never {@code null} + */ + private Node parseComparison() { + var lhs = parseAddition(); + while( true ) { + int idx=0; boolean negate=false; + // Test for any local nodes made, and "keep" lhs during peepholes + if( false ) ; + else if( match("==") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); } + else if( match("!=") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); negate=true; } + else if( match("<=") ) { idx=2; lhs = new BoolNode.LE(lhs, null); } + else if( match("<" ) ) { idx=2; lhs = new BoolNode.LT(lhs, null); } + else if( match(">=") ) { idx=1; lhs = new BoolNode.LE(null, lhs); } + else if( match(">" ) ) { idx=1; lhs = new BoolNode.LT(null, lhs); } + else break; + // Peepholes can fire, but lhs is already "hooked", kept alive + lhs.setDef(idx,parseAddition()); + lhs = lhs.peephole(); + if( negate ) // Extra negate for != + lhs = new NotNode(lhs).peephole(); + } + return lhs; + } + + /** + * Parse an additive expression + * + *
+     *     additiveExpr : multiplicativeExpr (('+' | '-') multiplicativeExpr)*
+     * 
+ * @return an add expression {@link Node}, never {@code null} + */ + private Node parseAddition() { + Node lhs = parseMultiplication(); + while( true ) { + if( false ) ; + else if( match("+") ) lhs = new AddNode(lhs,null); + else if( match("-") ) lhs = new SubNode(lhs,null); + else break; + lhs.setDef(2,parseMultiplication()); + lhs = lhs.peephole(); + } + return lhs; + } + + /** + * Parse an multiplicativeExpr expression + * + *
+     *     multiplicativeExpr : unaryExpr (('*' | '/') unaryExpr)*
+     * 
+ * @return a multiply expression {@link Node}, never {@code null} + */ + private Node parseMultiplication() { + var lhs = parseUnary(); + while( true ) { + if( false ) ; + else if( match("*") ) lhs = new MulNode(lhs,null); + else if( match("/") ) lhs = new DivNode(lhs,null); + else break; + lhs.setDef(2,parseUnary()); + lhs = lhs.peephole(); + } + return lhs; + } + + /** + * Parse a unary minus expression. + * + *
+     *     unaryExpr : ('-') | '!') unaryExpr | postfixExpr | primaryExpr
+     * 
+ * @return a unary expression {@link Node}, never {@code null} + */ + private Node parseUnary() { + if (match("-")) return new MinusNode(parseUnary()).peephole(); + if (match("!")) return new NotNode(parseUnary()).peephole(); + return parsePostfix(parsePrimary()); + } + + /** + * Parse a primary expression: + * + *
+     *     primaryExpr : integerLiteral | Identifier | true | false | null | new Identifier | '(' expression ')'
+     * 
+ * @return a primary {@link Node}, never {@code null} + */ + private Node parsePrimary() { + if( _lexer.isNumber() ) return parseIntegerLiteral(); + 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); + if( n!=null ) return n; + 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(); + for( Field field : obj._fields ) { + mem(new StoreNode(field._fname, mem(), n, initValue).peephole()); + } + return n.unkeep(); + } + + // Memory is one hidden SSA variable, including across branches and loops. + private Node mem() { return _scope.lookup("$mem"); } + private Node mem(Node n) { return _scope.update("$mem",n); } + + /** + * Parse postfix expression. For now this is just a field + * expression, but in future could be array index too. + * + *
+     *     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() + "'"); + + if( match("=") ) { + // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment + if( peek('=') ) _lexer._position--; + else { + Node val = parseExpression(); + mem(new StoreNode(name, mem(), expr, val).peephole()); + return expr; // "obj.a = expr" returns the expression while updating memory + } + } + + Type declaredType = ptr._obj._fields[idx]._type; + return parsePostfix(new LoadNode(name, declaredType, mem(), expr).peephole()); + } + + /** + * Parse integer literal + * + *
+     *     integerLiteral: [1-9][0-9]* | [0]
+     * 
+ */ + private ConstantNode parseIntegerLiteral() { + return (ConstantNode) new ConstantNode(_lexer.parseNumber()).peephole(); + } + + ////////////////////////////////// + // Utilities for lexical analysis + + // Return true and skip if "syntax" is next in the stream. + private boolean match (String syntax) { return _lexer.match (syntax); } + // Match must be "exact", not be followed by more id letters + 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() { + String id = _lexer.matchId(); + if (id != null && !KEYWORDS.contains(id) ) return id; + throw error("Expected an identifier, found '"+id+"'"); + } + + + + // Require an exact match + 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); + } + + RuntimeException errorSyntax(String syntax) { + return error("Syntax error, expected " + syntax + ": " + _lexer.getAnyNextToken()); + } + + public static RuntimeException error( String errorMessage ) { + return new RuntimeException(errorMessage); + } + + //////////////////////////////////// + // Lexer components + + private static class Lexer { + + // Input buffer; an array of text bytes read from a file or a string + private final byte[] _input; + // Tracks current position in input buffer + private int _position = 0; + + /** + * Record the source text for lexing + */ + public Lexer(String source) { + this(source.getBytes()); + } + + /** + * Direct from disk file source + */ + public Lexer(byte[] buf) { + _input = buf; + } + + // Very handy in the debugger, shows the unparsed program + @Override + public String toString() { + return new String(_input, _position, _input.length - _position); + } + + // True if at EOF + private boolean isEOF() { + return _position >= _input.length; + } + + // Peek next character, or report EOF + private char peek() { + return isEOF() ? Character.MAX_VALUE // Special value that causes parsing to terminate + : (char) _input[_position]; + } + + private char nextChar() { + char ch = peek(); + _position++; + return ch; + } + + // True if a white space + private boolean isWhiteSpace() { + return peek() <= ' '; // Includes all the use space, tab, newline, CR + } + + /** + * Return the next non-white-space character + */ + private void skipWhiteSpace() { + while (isWhiteSpace()) _position++; + } + + + // Return true, if we find "syntax" after skipping white space; also + // then advance the cursor past syntax. + // Return false otherwise, and do not advance the cursor. + boolean match(String syntax) { + skipWhiteSpace(); + int len = syntax.length(); + if (_position + len > _input.length) return false; + for (int i = 0; i < len; i++) + if ((char) _input[_position + i] != syntax.charAt(i)) + return false; + _position += len; + 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; + _position -= syntax.length(); + return false; + } + + private boolean peek(char ch) { + skipWhiteSpace(); + return peek()==ch; + } + + boolean peekIsId() { + skipWhiteSpace(); + return isIdStart(peek()); + } + + // Return an identifier or null + String matchId() { + return peekIsId() ? parseId() : null; + } + + // Used for errors + String getAnyNextToken() { + if (isEOF()) return ""; + if (isIdStart(peek())) return parseId(); + if (isNumber(peek())) return parseNumberString(); + if (isPunctuation(peek())) return parsePunctuation(); + return String.valueOf(peek()); + } + + boolean isNumber() {return isNumber(peek());} + boolean isNumber(char ch) {return Character.isDigit(ch);} + + private Type parseNumber() { + String snum = parseNumberString(); + if (snum.length() > 1 && snum.charAt(0) == '0') + throw error("Syntax error: integer values cannot start with '0'"); + return TypeInteger.constant(Long.parseLong(snum)); + } + private String parseNumberString() { + int start = _position; + while (isNumber(nextChar())) ; + return new String(_input, start, --_position - start); + } + + // First letter of an identifier + private boolean isIdStart(char ch) { + return Character.isAlphabetic(ch) || ch == '_'; + } + + // All characters of an identifier, e.g. "_x123" + private boolean isIdLetter(char ch) { + return Character.isLetterOrDigit(ch) || ch == '_'; + } + + private String parseId() { + int start = _position; + while (isIdLetter(nextChar())) ; + return new String(_input, start, --_position - start); + } + + private boolean isPunctuation(char ch) { + return "=;[]<>(){}+-/*!".indexOf(ch) != -1; + } + + private String parsePunctuation() { + int start = _position; + return new String(_input, start, 1); + } + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/Utils.java b/chapter10a/src/main/java/com/seaofnodes/simple/Utils.java new file mode 100644 index 00000000..4f2af769 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/Utils.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple; + +import java.util.ArrayList; + +public class Utils { + public static RuntimeException TODO() { return TODO("Not yet implemented"); } + public static RuntimeException TODO(String msg) { return new RuntimeException(msg); } + + /** + * Fast, constant-time, element removal. Does not preserve order + * + * @param array ArrayList to modify + * @param i element to be removed + * @return element removed + */ + public static E del(ArrayList array, int i) { + if ( i >= 0 && i < array.size() ) { + E tmp = array.get(i); + E last = array.removeLast(); + if (i < array.size()) array.set(i, last); + return tmp; + } + return null; + } + + /** + * 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( ArrayList ary, E x ) { + for( int i=0; i= 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/chapter10a/src/main/java/com/seaofnodes/simple/node/AddNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/AddNode.java new file mode 100644 index 00000000..de5044d7 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -0,0 +1,176 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.*; + +import java.util.BitSet; + +public class AddNode extends Node { + public AddNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Add"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("+"), visited); + return sb.append(")"); + } + + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if (i0.isConstant() && i1.isConstant()) + return TypeInteger.constant(i0.value()+i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize () { + Node lhs = in(1); + Node rhs = in(2); + Type t2 = rhs._type; + + // Add of 0. We do not check for (0+x) because this will already + // canonicalize to (x+0) + if( t2 instanceof TypeInteger i && i.value()==0 ) + return lhs; + + // Add of same to a multiply by 2 + if( lhs==rhs ) + return new MulNode(lhs,new ConstantNode(TypeInteger.constant(2)).peephole()); + + // Goal: a left-spine set of adds, with constants on the rhs (which then fold). + + // Move non-adds to RHS + if( !(lhs instanceof AddNode) && rhs instanceof AddNode ) + return swap12(); + + // x+(-y) becomes x-y + if( rhs instanceof MinusNode minus ) + return new SubNode(lhs,minus.in(1)); + + // Now we might see (add add non) or (add non non) or (add add add) but never (add non add) + + // Do we have x + (y + z) ? + // Swap to (x + y) + z + // Rotate (add add add) to remove the add on RHS + if( rhs instanceof AddNode add ) + return new AddNode(new AddNode(lhs,add.in(1)).peephole(), add.in(2)); + + // Now we might see (add add non) or (add non non) but never (add non add) nor (add add add) + if( !(lhs instanceof AddNode) ) + // Rotate; look for (add (phi cons) con/(phi cons)) + return spine_cmp(lhs,rhs,this) ? swap12() : phiCon(this,true); + + // Now we only see (add add non) + + // Dead data cycle; comes about from dead infinite loops. Do nothing, + // the loop will peep as dead after a bit. + if( lhs.in(1) == lhs ) + return null; + + // Do we have (x + con1) + con2? + // Replace with (x + (con1+con2) which then fold the constants + // lhs.in(2) is con1 here + // If lhs.in(2) is not a constant, we add ourselves as a dependency + // because if it later became a constant then we could make this + // transformation. + if( lhs.in(2).addDep(this)._type.isConstant() && rhs._type.isConstant() ) + return new AddNode(lhs.in(1),new AddNode(lhs.in(2),rhs).peephole()); + + + // 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 = phiCon(this,true); + if( phicon!=null ) return phicon; + + // Now we sort along the spine via rotates, to gather similar things together. + + // Do we rotate (x + y) + z + // into (x + z) + y ? + if( spine_cmp(lhs.in(2),rhs,this) ) + return new AddNode(new AddNode(lhs.in(1),rhs).peephole(),lhs.in(2)); + + return null; + } + + // Rotation is only valid for associative ops, e.g. Add, Mul, And, Or. + // Do we have ((phi cons)|(x + (phi cons)) + con|(phi cons)) ? + // Push constant up through the phi: x + (phi con0+con0 con1+con1...) + static Node phiCon(Node op, boolean rotate) { + Node lhs = op.in(1); + Node rhs = op.in(2); + // LHS is either a Phi of constants, or another op with Phi of constants + PhiNode lphi = pcon(lhs,op); + if( rotate && lphi==null && lhs.nIns() > 2 ) { + // Only valid to rotate constants if both are same associative ops + if( lhs.getClass() != op.getClass() ) return null; + lphi = pcon(lhs.in(2),op); // Will rotate with the Phi push + } + if( lphi==null ) return null; + + // RHS is a constant or a Phi of constants + if( !(rhs instanceof ConstantNode) && pcon(rhs,op)==null ) + return null; + + // If both are Phis, must be same Region + if( rhs instanceof PhiNode && lphi.in(0) != rhs.in(0) ) + return null; + + // Note that this is the exact reverse of Phi pulling a common op down + // to reduce total op-count. We don't get in an endless push-up + // push-down peephole cycle because the constants all fold first. + Node[] ns = new Node[lphi.nIns()]; + ns[0] = lphi.in(0); + // Push constant up through the phi: x + (phi con0+con0 con1+con1...) + for( int i=1; i hi._nid; + } + + @Override Node copy(Node lhs, Node rhs) { return new AddNode(lhs,rhs); } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/BoolNode.java new file mode 100644 index 00000000..bf0d3a84 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/BoolNode.java @@ -0,0 +1,76 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; + +import java.util.BitSet; + +abstract public class BoolNode extends Node { + + public BoolNode(Node lhs, Node rhs) { + super(null, lhs, rhs); + } + + abstract String op(); // String opcode name + + @Override + public String label() { return getClass().getSimpleName(); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append(op()), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if (i0.isConstant() && i1.isConstant()) + return TypeInteger.constant(doOp(i0.value(), i1.value()) ? 1 : 0); + } + return TypeInteger.BOT; + } + + abstract boolean doOp(long lhs, long rhs); + + @Override + public Node idealize() { + // Compare of same + if( in(1)==in(2) ) + return new ConstantNode(TypeInteger.constant(doOp(3,3)?1:0)); + + // Equals pushes constant to the right; 5==X becomes X==5. + if( this instanceof EQ ) { + if( !(in(2) instanceof ConstantNode) ) { + // con==noncon becomes noncon==con + if( in(1) instanceof ConstantNode ) + return new EQ(in(2),in(1)); + // Equals sorts by NID otherwise: non.high == non.low becomes non.low == non.high + else if( in(1)._nid > in(2)._nid ) + return new EQ(in(2),in(1)); + } + // Equals X==0 becomes a !X + if( (in(2)._type == TypeInteger.ZERO || in(2)._type == TypeMemPtr.NULLPTR) ) + return new NotNode(in(1)); + } + + // Do we have ((phi cons) cmp con) ? + // Do we have ((phi cons) cmp (phi cons)) ? + // Push the compare up through the phi: (phi con0 cmp con, con1 cmp con...) + Node phicon = AddNode.phiCon(this,this instanceof EQ); + if( phicon!=null ) return phicon; + + return null; + } + + public static class EQ extends BoolNode { public EQ(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "=="; } boolean doOp(long lhs, long rhs) { return lhs == rhs; } Node copy(Node lhs, Node rhs) { return new EQ(lhs,rhs); } } + public static class LT extends BoolNode { public LT(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "<" ; } boolean doOp(long lhs, long rhs) { return lhs < rhs; } Node copy(Node lhs, Node rhs) { return new LT(lhs,rhs); } } + public static class LE extends BoolNode { public LE(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "<="; } boolean doOp(long lhs, long rhs) { return lhs <= rhs; } Node copy(Node lhs, Node rhs) { return new LE(lhs,rhs); } } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/CastNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/CastNode.java new file mode 100644 index 00000000..fec540f7 --- /dev/null +++ b/chapter10a/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/chapter10a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java new file mode 100644 index 00000000..d760c0df --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java @@ -0,0 +1,54 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; + +import java.util.BitSet; + +/** + * A Constant node represents a constant value. At present, the only constants + * that we allow are integer literals; therefore Constants contain an integer + * value. As we add other types of constants, we will refactor how we represent + * Constants. + *

+ * Constants have no semantic inputs. However, we set Start as an input to + * Constants to enable a forward graph walk. This edge carries no semantic + * meaning, and it is present solely to allow visitation. + *

+ * The Constant's value is the value stored in it. + */ +public class ConstantNode extends Node { + final Type _con; + public ConstantNode( Type type ) { + super(Parser.START); + _con = type; + } + + @Override + public String label() { return "#"+_con; } + + @Override + public String uniqueName() { return "Con_" + _nid; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return _con.print(sb); + } + + @Override public boolean isMultiTail() { return true; } + + @Override + public Type compute() { return _con; } + + @Override + public Node idealize() { return null; } + + @Override + boolean eq(Node n) { + ConstantNode con = (ConstantNode)n; // Contract + return _con==con._con; + } + + @Override + int hash() { return _con.hashCode(); } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/DivNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/DivNode.java new file mode 100644 index 00000000..6cee90ad --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/DivNode.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class DivNode extends Node { + public DivNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Div"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("/"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + if (t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1) { + if (i0.isConstant() && i1.isConstant()) + return i1.value() == 0 + ? TypeInteger.ZERO + : TypeInteger.constant(i0.value()/i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + // Div of 1. + if( in(2)._type.isConstant() && in(2)._type instanceof TypeInteger i && i.value()==1 ) + return in(1); + return null; + } + + @Override Node copy(Node lhs, Node rhs) { return new DivNode(lhs,rhs); } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/IfNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/IfNode.java new file mode 100644 index 00000000..1cf4ddaf --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/IfNode.java @@ -0,0 +1,62 @@ +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 + public String label() { return "If"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("if( "); + return in(1)._print0(sb, visited).append(" )"); + } + + @Override public boolean isCFG() { return true; } + @Override public boolean isMultiHead() { return true; } + + public Node ctrl() { return in(0); } + public Node pred() { return in(1); } + + @Override + public Type compute() { + // If the If node is not reachable then neither is any following Proj + if (ctrl()._type != Type.CONTROL && ctrl()._type != Type.BOTTOM ) + return TypeTuple.IF_NEITHER; + Node pred = pred(); + Type t = pred._type; + // High types mean NEITHER side is reachable. + // Wait until the type falls to decide which way to go. + if( t == Type.TOP || t == TypeInteger.TOP ) + return TypeTuple.IF_NEITHER; + // If constant is 0 then false branch is reachable + // Else true branch is reachable + if (t instanceof TypeInteger ti && ti.isConstant()) + return ti==TypeInteger.ZERO ? TypeTuple.IF_FALSE : TypeTuple.IF_TRUE; + + return TypeTuple.IF_BOTH; + } + + @Override + public Node idealize() { + // Hunt up the immediate dominator tree. If we find an identical if + // test on either the true or false branch, that side wins. + if( !pred()._type.isHighOrConst() ) + for( Node dom = idom(), prior=this; dom!=null; prior = dom, dom = dom.idom() ) + if( dom.addDep(this) instanceof IfNode iff && iff.pred().addDep(this)==pred() && prior instanceof ProjNode prj ) { + setDef(1,new ConstantNode(TypeInteger.make(true,prj._idx==0?1:0)).peephole()); + return this; + } + return null; + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/LoadNode.java new file mode 100644 index 00000000..54689283 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -0,0 +1,72 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; + +import java.util.BitSet; + +/** + * Load represents extracting a value from inside a memory object, + * in chapter 10 this means Struct fields. + */ +public class LoadNode extends MemOpNode { + + Type _declaredType; + /** + * Load a value from a ptr.field. + * + * @param name The field we are loading + * @param memSlice The whole-memory node - this is updated after a Store + * @param memPtr The ptr to the struct from where we load a field + */ + public LoadNode(String name, Type glb, Node memSlice, Node memPtr) { + super(name, memSlice, memPtr, null); + _declaredType = glb; + } + + @Override + public String label() { return "Load"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { return sb.append(".").append(_name); } + + @Override + public Type compute() { + return _declaredType; + } + + @Override + public Node idealize() { + + // Simple Load-after-Store on same address. + if( mem() instanceof StoreNode st && + ptr() == st.ptr() && _name.equals(st._name) ) { // Must check same object + return st.val(); + } + + // Push a Load up through a Phi, as long as it collapses on at least + // one arm. If at a Loop, the backedge MUST collapse - else we risk + // spinning the same transform around the loop indefinitely. + // BEFORE (2 Sts, 1 Ld): AFTER (1 St, 0 Ld): + // if( pred ) ptr.x = e0; val = pred ? e0 + // else ptr.x = e1; : e1; + // val = ptr.x; ptr.x = val; + if( mem() instanceof PhiNode memphi && memphi.region()._type == Type.CONTROL && memphi.nIns()== 3 ) { + // Profit on RHS/Loop backedge + if( profit(memphi,2) || + // Else must not be a loop to count profit on LHS. + (!(memphi.region() instanceof LoopNode) && profit(memphi,1)) ) { + Node ld1 = new LoadNode(_name,_declaredType,memphi.in(1),ptr()).peephole(); + Node ld2 = new LoadNode(_name,_declaredType,memphi.in(2),ptr()).peephole(); + return new PhiNode(_name,_type,memphi.region(),ld1,ld2); + } + } + + return null; + } + + // Profitable if we find a matching Store on this Phi arm. + private boolean profit(PhiNode phi, int idx) { + Node px = phi.in(idx); + return px!=null && px.addDep(this) instanceof StoreNode st1 && ptr()==st1.ptr() && _name.equals(st1._name); + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/LoopNode.java new file mode 100644 index 00000000..a0ba973e --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/LoopNode.java @@ -0,0 +1,31 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; + +import java.util.BitSet; + +public class LoopNode extends RegionNode { + + public LoopNode( Node entry ) { super(null,entry,null); } + + Node entry() { return in(1); } + Node back () { return in(2); } + + @Override + public String label() { return "Loop"; } + + @Override + public Type compute() { + if( inProgress() ) return Type.CONTROL; + return entry()._type; + } + + @Override + public Node idealize() { + return inProgress() ? null : super.idealize(); + } + + // Bypass Region idom, same as the default idom() using use entry in(1) instead of in(0) + @Override int idepth() { return _idepth!=0 ? _idepth : cacheIDepth(entry().idepth()+1); } + @Override Node idom() { return entry(); } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java new file mode 100644 index 00000000..d811e71a --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java @@ -0,0 +1,37 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMemPtr; + +/** + * Convenience common base for Load and Store. + */ +public abstract class MemOpNode extends Node { + + public final String _name; + + public MemOpNode(String name, Node memSlice, Node memPtr, Node value) { + super(null, memSlice, memPtr, value); + _name = name; + } + + public Node mem() { return in(1); } + public Node ptr() { return in(2); } + + @Override + boolean eq(Node n) { + MemOpNode mem = (MemOpNode)n; // Invariant + return _name.equals(mem._name); + } + + @Override + int hash() { return _name.hashCode(); } + + @Override + String err() { + Type ptr = ptr()._type; + return (ptr==Type.BOTTOM || (ptr instanceof TypeMemPtr tmp && tmp._nil) ) + ? "Might be null accessing '" + _name + "'" + : null; + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/MinusNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/MinusNode.java new file mode 100644 index 00000000..8b1b73bf --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/MinusNode.java @@ -0,0 +1,34 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class MinusNode extends Node { + public MinusNode(Node in) { super(null, in); } + + @Override public String label() { return "Minus"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("(-"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + if (in(1)._type instanceof TypeInteger i0) + return i0.isConstant() ? TypeInteger.constant(-i0.value()) : i0; + return in(1)._type==Type.TOP ? TypeInteger.TOP : TypeInteger.BOT; + } + + @Override + public Node idealize() { + // -(-x) is x + if( in(1) instanceof MinusNode minus ) + return minus.in(1); + + return null; + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/MulNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/MulNode.java new file mode 100644 index 00000000..97721ce3 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/MulNode.java @@ -0,0 +1,59 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class MulNode extends Node { + public MulNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Mul"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("*"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + if (t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1) { + if (i0.isConstant() && i1.isConstant()) + return TypeInteger.constant(i0.value()*i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Mul of 1. We do not check for (1*x) because this will already + // canonicalize to (x*1) + if ( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==1 ) + return lhs; + + // Move constants to RHS: con*arg becomes arg*con + if ( t1.isConstant() && !t2.isConstant() ) + return swap12(); + + // 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,true); + if( phicon!=null ) return phicon; + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new MulNode(lhs,rhs); } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/MultiNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/MultiNode.java new file mode 100644 index 00000000..2743bd19 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/MultiNode.java @@ -0,0 +1,14 @@ +package com.seaofnodes.simple.node; + +public abstract class MultiNode extends Node { + + public MultiNode(Node... inputs) { super(inputs); } + + // Find a projection by index + ProjNode proj( int idx ) { + for( Node out : _outputs ) + if( out instanceof ProjNode prj && prj._idx==idx ) + return prj; + return null; + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/NewNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/NewNode.java new file mode 100644 index 00000000..b13462f5 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/NewNode.java @@ -0,0 +1,37 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMemPtr; + +import java.util.BitSet; + +public class NewNode extends Node { + + TypeMemPtr _ptr; + + public NewNode(TypeMemPtr ptr, Node ctrl) { + super(ctrl); + this._ptr = ptr; + } + + @Override + public String label() { return "new"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("new "); + return sb.append(_ptr._obj.str()); + } + + @Override + public Type compute() { return _ptr; } + + @Override + public Node idealize() { return null; } + + @Override + boolean eq(Node n) { return this == n; } + + @Override + int hash() { return _ptr.hashCode(); } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/Node.java new file mode 100644 index 00000000..eb6c1697 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/Node.java @@ -0,0 +1,633 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; + +import com.seaofnodes.simple.IRPrinter; +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.Type; + +import java.util.*; +import java.util.function.Function; + +/** + * All Nodes in the Sea of Nodes IR inherit from the Node class. + * The Node class provides common functionality used by all subtypes. + * Subtypes of Node specialize by overriding methods. + */ +public abstract class Node { + + /** + * Each node has a unique dense Node ID within a compilation context + * The ID is useful for debugging, for using as an offset in a bitvector, + * as well as for computing equality of nodes (to be implemented later). + */ + public final int _nid; + + /** + * Inputs to the node. These are use-def references to Nodes. + *

+ * Generally fixed length, ordered, nulls allowed, no unused trailing space. + * Ordering is required because e.g. "a/b" is different from "b/a". + * The first input (offset 0) is often a {@link #isCFG} node. + */ + public final ArrayList _inputs; + + /** + * Outputs reference Nodes that are not null and have this Node as an + * input. These nodes are users of this node, thus these are def-use + * references to Nodes. + *

+ * Outputs directly match inputs, making a directed graph that can be + * walked in either direction. These outputs are typically used for + * efficient optimizations but otherwise have no semantics meaning. + */ + public final ArrayList _outputs; + + + /** + * Current computed type for this Node. This value changes as the graph + * changes and more knowledge is gained about the program. + */ + public Type _type; + + + /** + * A private Global Static mutable counter, for unique node id generation. + * To make the compiler multithreaded, this field will have to move into a TLS. + * Starting with value 1, to avoid bugs confusing node ID 0 with uninitialized values. + * */ + private static int UNIQUE_ID = 1; + public static int UID() { return UNIQUE_ID; } + + protected Node(Node... inputs) { + _nid = UNIQUE_ID++; // allocate unique dense ID + _inputs = new ArrayList<>(); + Collections.addAll(_inputs,inputs); + _outputs = new ArrayList<>(); + for( Node n : _inputs ) + if( n != null ) + n.addUse( this ); + } + + // Easy reading label for debugger, e.g. "Add" or "Region" or "EQ" + public abstract String label(); + + // Unique label for debugging, e.g. "Add12" or "Region30" or "EQ99" + public String uniqueName() { + return label() + _nid; + } + + // ------------------------------------------------------------------------ + + // Debugger Printing. + + // {@code toString} is what you get in the debugger. It has to print 1 + // line (because this is what a debugger typically displays by default) and + // has to be robust with broken graph/nodes. + @Override + public final String toString() { return print(); } + + // This is a *deep* print. This version will fail on cycles, which we will + // correct later when we can parse programs with loops. We print with a + // tik-tok style; the common _print0 calls the per-Node _print1, which + // calls back to _print0; + public final String print() { + return _print0(new StringBuilder(), new BitSet()).toString(); + } + + // This is the common print: check for repeats, check for DEAD and print + // "DEAD" else call the per-Node print1. + final StringBuilder _print0(StringBuilder sb, BitSet visited) { + if (visited.get(_nid) && !(this instanceof ConstantNode) ) + return sb.append(label()); + visited.set(_nid); + return isDead() + ? sb.append(uniqueName()).append(":DEAD") + : _print1(sb, visited); + } + // Every Node implements this; a partial-line recursive print + abstract StringBuilder _print1(StringBuilder sb, BitSet visited); + + public String p(int depth) { return IRPrinter.prettyPrint(this,depth); } + + public boolean isMultiHead() { return false; } + public boolean isMultiTail() { return false; } + + // ------------------------------------------------------------------------ + // Graph Node & Edge manipulation + + /** + * Gets the ith input node + * @param i Offset of the input node + * @return Input node or null + */ + public Node in(int i) { return _inputs.get(i); } + public Node out(int i) { return _outputs.get(i); } + + public int nIns() { return _inputs.size(); } + + public int nOuts() { return _outputs.size(); } + + public boolean isUnused() { return nOuts() == 0; } + + public boolean isCFG() { return false; } + + public boolean isMem() { return false; } + + /** + * Change a def into a Node. Keeps the edges correct, by removing + * the corresponding use->def edge. This may make the original + * def go dead. This function is co-recursive with {@link #kill}. + *

+ + * This method is the normal path for altering a Node, because it does the + * proper default edge maintenance. It also immediately kills + * Nodes that lose their last use; at times care must be taken to avoid + * killing Nodes that are being used without having an output Node. This + * definitely happens in the middle of recursive {@link #peephole} calls. + * + * @param idx which def to set + * @param new_def the new definition + * @return new_def for flow coding + */ + public Node setDef(int idx, Node new_def ) { + unlock(); + Node old_def = in(idx); + if( old_def == new_def ) return new_def; // No change + // If new def is not null, add the corresponding def->use edge + // This needs to happen before removing the old node's def->use edge as + // the new_def might get killed if the old node kills it recursively. + if( new_def != null ) + new_def.addUse(this); + if( old_def != null && // If the old def exists, remove a def->use edge + old_def.delUse(this) ) // If we removed the last use, the old def is now dead + 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; + } + + // Remove the numbered input, compressing the inputs in-place. This + // shuffles the order deterministically - which is suitable for Region and + // Phi, but not for every Node. If the def goes dead, it is recursively + // killed, which may include 'this' Node. + Node delDef(int idx) { + unlock(); + Node old_def = in(idx); + if( old_def != null && // If the old def exists, remove a def->use edge + old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + old_def.moveDepsToWorklist(); + Utils.del(_inputs, idx); + return this; + } + + /** + * Add a new def to an existing Node. Keep the edges correct by + * adding the corresponding def->use edge. + * + * @param new_def the new definition, appended to the end of existing definitions + * @return new_def for flow coding + */ + Node addDef(Node new_def) { + unlock(); + // Add use->def edge + _inputs.add(new_def); + // If new def is not null, add the corresponding def->use edge + if( new_def != null ) + new_def.addUse(this); + return new_def; + } + + // Breaks the edge invariants, used temporarily + protected void addUse(N n) { _outputs.add(n); } + + // Remove node 'use' from 'def's (i.e. our) output list, by compressing the list in-place. + // Return true if the output list is empty afterward. + // Error is 'use' does not exist; ok for 'use' to be null. + protected boolean delUse( Node use ) { + Utils.del(_outputs, Utils.find(_outputs, use)); + return _outputs.isEmpty(); + } + + // Shortcut for "popping" n nodes. A "pop" is basically a + // setDef(last,null) followed by lowering the nIns() count. + void popN(int n) { + unlock(); + for( int i=0; iuses, by setting all of its defs + * to null. This may recursively kill more Nodes, and is basically dead + * code elimination. + */ + public void kill( ) { + unlock(); + assert isUnused(); // Has no uses, so it is dead + _type=null; // Flag as dead + while( nIns()>0 ) { // Set all inputs to null, recursively killing unused Nodes + Node old_def = _inputs.removeLast(); + if( old_def != null ) { + IterPeeps.add(old_def);// Revisit neighbor because removed use + if( old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + } + } + assert isDead(); // Really dead now + } + + // Mostly used for asserts and printing. + public boolean isDead() { return isUnused() && nIns()==0 && _type==null; } + + // Shortcuts to stop DCE mid-parse + // Add bogus null use to keep node alive + public N keep() { addUse(null); return (N)this; } + // Remove bogus null. + 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 + public void subsume( Node nnn ) { + assert nnn!=this; + while( nOuts() > 0 ) { + Node n = _outputs.removeLast(); + n.unlock(); + int idx = Utils.find(n._inputs, this); + n._inputs.set(idx,nnn); + nnn.addUse(n); + } + kill(); + } + + // ------------------------------------------------------------------------ + // Graph-based optimizations + + /** + * Try to peephole at this node and return a better replacement Node. + * Always returns some not-null Node (often this). + */ + public final Node peephole() { + var obs = IterPeeps.midAssert() ? null : Parser.PARSER == null ? null : Parser.PARSER._obs; + if( obs != null ) obs.before(this); + Node n = peepholeOpt(); + Node rez = n == null ? this : deadCodeElim(n.peephole()); + if( obs != null ) obs.after(this, n == null ? null : rez, false); + return rez; + } + + /** + * Try to peephole at this node and return a better replacement Node if + * possible. We compute a {@link Type} and then check and replace: + *

    + *
  • if the Type {@link Type#isConstant}, we replace with a {@link ConstantNode}
  • + *
  • in a future chapter we will look for a + * Common Subexpression + * to eliminate.
  • + *
  • we ask the Node for a better replacement. The "better replacement" + * is things like {@code (1+2)} becomes {@code 3} and {@code (1+(x+2))} becomes + * {@code (x+(1+2))}. By canonicalizing expressions we fold common addressing + * math constants, remove algebraic identities and generally simplify the + * code.
  • + * + * Unlike peephole above, this explicitly returns null for no-change, or not-null + * for a better replacement (which can be this). + *
+ */ + public final Node peepholeOpt( ) { + ITER_CNT++; + // Compute initial or improved Type + Type old = setType(compute()); + + // Replace constant computations from non-constants with a constant node + if( !(this instanceof ConstantNode) && _type.isHighOrConst() ) + return new ConstantNode(_type).peepholeOpt(); + + // Global Value Numbering + if( _hash==0 ) { + Node n = GVN.get(this); // Will set _hash as a side effect + if( n==null ) + GVN.put(this,this); // Put in table now + else { + // Because of random worklist ordering, the two equal nodes + // might have different types. Because of monotonicity, both + // types are valid. To preserve monotonicity, the resulting + // shared Node has to have the best of both types. + n.setType(n._type.join(_type)); + _hash = 0; // Clear, since it never went in the table + return deadCodeElim(n);// Return previous; does Common Subexpression Elimination + } + } + + // Ask each node for a better replacement + Node n = idealize(); + if( n != null ) // Something changed + return n; // Report progress + + if( old==_type ) ITER_NOP_CNT++; + return old==_type ? null : this; // Report progress + } + public static int ITER_CNT, ITER_NOP_CNT; + + // m is the new Node, self is the old. + // Return 'm', which may have zero uses but is alive nonetheless. + // If self has zero uses (and is not 'm'), {@link #kill} self. + 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() && !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(). + m.keep(); // Keep m alive + kill(); // Kill self because replacing with 'm' + m.unkeep(); // Okay to peephole m + } + return m; + } + + /** + * This function needs to be + * Monotonic + * as it is part of a Monotone Analysis Framework. + * See for example this set of slides. + *

+ * For Chapter 2, all our Types are really integer constants, and so all + * the needed properties are trivially true, and we can ignore the high + * theory. Much later on, this will become important and allow us to do + * many fancy complex optimizations trivially... because theory. + *

+ * {@link #compute()} needs to be stand-alone, and cannot recursively call compute + * on its inputs, because programs are cyclic (have loops!) and this will just + * infinitely recurse until stack overflow. Instead, compute typically + * computes a new type from the {@link #_type} field of its inputs. + */ + public abstract Type compute(); + + // Set the type. Assert monotonic progress. + // If changing, add users to worklist. + public Type setType(Type type) { + Type old = _type; + assert old==null || type.isa(old); // Since _type not set, can just re-run this in assert in the debugger + if( old == type ) return old; + _type = type; // Set _type late for easier assert debugging + IterPeeps.addAll(_outputs); + moveDepsToWorklist(); + return old; + } + + + /** + * This function rewrites the current Node into a more "idealized" form. + * This is the bulk of our peephole rewrite rules, and we use this to + * e.g. turn arbitrary collections of adds and multiplies with mixed + * constants into a normal form that's easy for hardware to implement. + * Example: An array addressing expression: + *

   ary[idx+1]
+ * might turn into Sea-of-Nodes IR: + *
   (ary+12)+((idx+1) * 4)
+ * This expression can then be idealized into: + *
   ary + ((idx*4) + (12 + (1*4)))
+ * And more folding: + *
   ary + ((idx<<2) + 16)
+ * And during code-gen: + *
   MOV4 Rary,Ridx,16 // or some such hardware-specific notation 
+ *

+ * {@link #idealize} has a very specific calling convention: + *

    + *
  • If NO change is made, return {@code null} + *
  • If ANY change is made, return not-null; this can be {@code this} + *
  • The returned Node does NOT call {@link #peephole} on itself; the {@link #peephole} call will recursively peephole it. + *
  • Any NEW nodes that are not directly returned DO call {@link #peephole}. + *
+ *

+ * Examples: + * + * + * + * + * + *
before after return comment
{@code (x+5) }{@code (x+5) }{@code null }No change
{@code (5+x) }{@code (x+5) }{@code this }Swapped arguments
{@code ((x+1)+2)}{@code (x+(1+2))}{@code (x+_) }Returns 2 new Nodes
+ * + * The last entry deserves more discussion. The new Node {@code (1+2)} + * created in {@link #idealize} calls {@link #peephole} (which then folds + * into a constant). The other new Node {@code (x+3)} does not call + * peephole, because it is returned and peephole itself will recursively + * call peephole. + *

+ * Since idealize calls peephole and peephole calls idealize, you must be + * careful that all idealizations are monotonic: all transforms + * remove some feature, so that the set of available transforms always + * shrinks. If you don't, you risk an infinite peephole loop! + * + * @return Either a new or changed node, or null for no changes. + */ + public abstract Node idealize(); + + + // Some of the peephole rules get complex, and search further afield than + // just the nearest neighbor. These peepholes can fail the pattern match + // on a node some distance away, and if that node ever changes we should + // retry the peephole. Track a set of Nodes dependent on `this`, and + // revisit them if `this` changes. + ArrayList _deps; + public int nDeps() { return _deps == null ? 0 : _deps.size(); } + public Node dep(int idx) { return _deps.get(idx); } + + /** + * 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 + // like adding dependencies. + if( IterPeeps.midAssert() ) return this; + var obs = IterPeeps.midAssert() ? null : Parser.PARSER == null ? null : Parser.PARSER._obs; + if( obs != null ) obs.dep(this, dep); + if( _deps==null ) _deps = new ArrayList<>(); + if( Utils.find(_deps ,dep) != -1 ) return this; // Already on list + if( Utils.find(_inputs,dep) != -1 ) return this; // No need for deps on immediate neighbors + if( Utils.find(_outputs,dep)!= -1 ) return this; + _deps.add(dep); + return this; + } + + // Move the dependents onto a worklist, and clear for future dependents. + public void moveDepsToWorklist( ) { + if( _deps==null ) return; + IterPeeps.addAll(_deps); + _deps.clear(); + } + + + // Global Value Numbering. Hash over opcode and inputs; hits in this table + // are structurally equal. + public static final HashMap GVN = new HashMap<>(); + + // Two nodes are equal if they have the same inputs and the same "opcode" + // which means the same Java class, plus same internal parts. + @Override public final boolean equals( Object o ) { + if( o==this ) return true; + if( o.getClass() != getClass() ) return false; + Node n = (Node)o; + int len = _inputs.size(); + if( len != n._inputs.size() ) return false; + for( int i=0; i>13) ^ n._nid; + if( hash==0 ) hash = 0xDEADBEEF; // Bad hash, so use some junky thing + return (_hash=hash); + } + // Subclasses add extra hash info (such as ConstantNodes constant) + int hash() { return 0; } + + + // ------------------------------------------------------------------------ + // Peephole utilities + + // Swap inputs without letting either input go dead during the swap. + Node swap12() { + unlock(); // Hash is order dependent + Node tmp = in(1); + _inputs.set(1,in(2)); + _inputs.set(2,tmp); + return this; + } + + /** + * 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 char _idepth; // IDOM depth approx; Zero is unset; non-zero is cached legit + + // Find the lowest common ancestor in the current dominator tree. + Node domLCA(Node rhs) { + if( rhs==null ) return this; + Node lhs = this; + while( lhs != rhs ) { + if( lhs==null || rhs==null ) return null; + int comp = lhs.idepth() - rhs.idepth(); + if( comp >= 0 ) lhs = lhs.idom(); + if( comp <= 0 ) rhs = rhs.idom(); + } + return lhs; + } + + int idepth() { return _idepth!=0 ? _idepth : cacheIDepth(idom().idepth()+1); } + // Zero depth means uncached. Check before narrowing so overflow cannot wrap. + final int cacheIDepth(int depth) { + assert 0 <= depth && depth <= Character.MAX_VALUE : "Dominator depth exceeds 65535"; + return _idepth = (char)depth; + } + + + // 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. + */ + public static void reset() { + UNIQUE_ID = 1; + GVN.clear(); + ITER_CNT = ITER_NOP_CNT = 0; + } + + + // Utility to walk the entire graph applying a function; return the first + // not-null result. + private static final BitSet WVISIT = new BitSet(); + final public E walk( Function pred ) { + assert WVISIT.isEmpty(); + E rez = _walk(pred); + WVISIT.clear(); + return rez; + } + + private E _walk( Function pred ) { + if( WVISIT.get(_nid) ) return null; // Been there, done that + WVISIT.set(_nid); + E x = pred.apply(this); + if( x != null ) return x; + for( Node def : _inputs ) if( def != null && (x = def._walk(pred)) != null ) return x; + for( Node use : _outputs ) if( use != null && (x = use._walk(pred)) != null ) return x; + return null; + } + + /** + * Debugging utility to find a Node by index + */ + public Node find(int nid) { + return walk( n -> n._nid==nid ? n : null ); + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/NotNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/NotNode.java new file mode 100644 index 00000000..83141836 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/NotNode.java @@ -0,0 +1,42 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +public class NotNode extends Node { + public NotNode(Node in) { super(null, in); } + + @Override public String label() { return "Not"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("(!"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t0 = in(1)._type; + switch( t0 ) { + case TypeInteger i0: + return i0.isConstant() ? TypeInteger.constant(i0.value()==0 ? 1 : 0) : i0; + case TypeMemPtr p0: + // top->top, bot->bot, null->1, *void->0, not-null ptr->0, ptr/nil->bot + // If input in null then true + // If input is not null ptr then false + if( p0 == TypeMemPtr.TOP ) return TypeInteger.TOP; + if( p0 == TypeMemPtr.NULLPTR ) return TypeInteger.constant(1); + if( !p0._nil ) return TypeInteger.constant(0); + return TypeInteger.BOT; + case Type t: + if( t0.getClass() != Type.class ) + // Only doing NOT on ints and ptrs + throw Utils.TODO(); + return t0==Type.TOP ? TypeInteger.TOP : TypeInteger.BOT; + } + } + + @Override + public Node idealize() { return null; } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/PhiNode.java new file mode 100644 index 00000000..bdf50847 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/PhiNode.java @@ -0,0 +1,157 @@ +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; + +public class PhiNode extends Node { + + final String _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; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + if( !(region() instanceof RegionNode r) || r.inProgress() ) + sb.append("Z"); + sb.append("Phi("); + for( Node in : _inputs ) { + if (in == null) sb.append("____"); + else in._print0(sb, visited); + sb.append(","); + } + sb.setLength(sb.length()-1); + sb.append(")"); + return sb; + } + + 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; + // 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 ) + t = t.meet(in(i)._type); + return t; + } + + @Override + public Node idealize() { + if( !(region() instanceof RegionNode r ) ) + return in(1); // Input has collapse to e.g. starting control. + if( r.inProgress() || r.nIns()<=1 ) + return null; // Input is in-progress + + // If we have only a single unique input, become it. + Node live = singleUniqueInput(); + if (live != null) + return live; + + // Pull "down" a common data op. One less op in the world. One more + // Phi, but Phis do not make code. + // Phi(op(A,B),op(Q,R),op(X,Y)) becomes + // op(Phi(A,Q,X), Phi(B,R,Y)). + Node op = in(1); + if( op.nIns()==3 && op.in(0)==null && !op.isCFG() && same_op() ) { + Node[] lhss = new Node[nIns()]; + Node[] rhss = new Node[nIns()]; + lhss[0] = rhss[0] = in(0); // Set Region + for( int i=1; i1 && in(nIns()-1) == null; + } + + // Never equal if inProgress + @Override boolean eq( Node n ) { + return !inProgress(); + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java new file mode 100644 index 00000000..f13f3708 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.*; + +import java.util.BitSet; + +/** Keeps the return value and all preceding memory effects alive. */ +public class ReturnNode extends Node { + + public ReturnNode(Node ctrl, Node data, ScopeNode scope) { + super(ctrl, data, scope.lookup("$mem")); + } + + public Node ctrl() { return in(0); } + public Node expr() { return in(1); } + + @Override + public String label() { return "Return"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("return "); + expr()._print0(sb, visited); + return sb.append(";"); + } + + @Override public boolean isCFG() { return true; } + + @Override + public Type compute() { + return TypeTuple.make(ctrl()._type,expr()._type); + } + + @Override + public Node idealize() { + if( ctrl()._type==Type.XCONTROL ) + return ctrl(); + return null; + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java new file mode 100644 index 00000000..455f4b3a --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -0,0 +1,304 @@ +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.*; + +/** + * The Scope node is purely a parser helper - it tracks names to nodes with a + * stack of scopes. + */ +public class ScopeNode extends Node { + + /** + * The control is a name that binds to the currently active control + * node in the graph + */ + public static final String CTRL = "$ctrl"; + public static final String ARG0 = "arg"; + + /** + * Names for every input edge + */ + 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; + } + + + @Override public String label() { return "Scope"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("Scope[ "); + String[] names = reverseNames(); + for( int j=0; j syms : _scopes ) + for( String name : syms.keySet() ) + names[syms.get(name)] = name; + return names; + } + + @Override public Type compute() { return Type.BOTTOM; } + + @Override public Node idealize() { return null; } + + 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, 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); + } + /** + * Lookup a name in all scopes starting from most deeply nested. + * + * @param name Name to be looked up + * @see #update(String, Node, int) + */ + public Node lookup( String name ) { return update(name,null,_scopes.size()-1); } + /** + * If the name is present in any scope, then redefine else null + * + * @param name Name being redefined + * @param n The node to bind to the name + */ + 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. + * + * @param name Name whose binding is being updated or looked up + * @param n If null, do a lookup, else update binding + * @param nestingLevel The starting nesting level + * @return node being looked up, or the one that was updated + */ + private Node update( String name, Node n, int nestingLevel ) { + if( nestingLevel<0 ) return null; // Missed in all scopes, not found + var syms = _scopes.get(nestingLevel); // Get the symbol table for nesting level + var idx = syms.get(name); + if( idx == null ) return update(name,n,nestingLevel-1); // Not found in this scope, recursively look in parent scope + Node old = in(idx); + if( old instanceof ScopeNode loop ) { + // Lazy Phi! + old = loop.in(idx) instanceof PhiNode phi && loop.ctrl()==phi.region() + // Loop already has a real Phi, use it + ? loop.in(idx) + // 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, 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); } + + /** + * The ctrl of a ScopeNode is always bound to the currently active + * control node in the graph, via a special name '$ctrl' that is not + * a valid identifier in the language grammar and hence cannot be + * referenced in Simple code. + * + * @param n The node to be bound to '$ctrl' + * + * @return Node that was bound + */ + public Node ctrl(Node n) { return setDef(0,n); } + + /** + * Duplicate a ScopeNode; including all levels, up to Nodes. So this is + * neither shallow (would dup the Scope but not the internal HashMap + * tables), nor deep (would dup the Scope, the HashMap tables, but then + * also the program Nodes). + *

+ * If the {@code loop} flag is set, the edges are filled in as the original + * Scope, as a indication of Lazy Phis at loop heads. The goal here is to + * not make Phis at loop heads for variables which are never touched in the + * loop body. + *

+ * The new Scope is a full-fledged Node with proper use<->def edges. + */ + public ScopeNode dup() { return dup(false); } + public ScopeNode dup(boolean loop) { + ScopeNode dup = new ScopeNode(); + // Our goals are: + // 1) duplicate the name bindings of the ScopeNode across all stack levels + // 2) Make the new ScopeNode a user of all the nodes bound + // 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 + for( int i=1; i { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); + if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); + return this; + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/StoreNode.java new file mode 100644 index 00000000..4bc67c3c --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -0,0 +1,66 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.*; + +import java.util.BitSet; + +/** + * Store represents setting a value to a memory based object, in chapter 10 + * this means a field inside a struct. + */ +public class StoreNode extends MemOpNode { + + /** + * @param name The struct field we are assigning to + * @param memSlice The whole-memory node - this is updated after a Store + * @param memPtr The ptr to the struct where we will store a value + * @param value Value to be stored + */ + public StoreNode(String name, Node memSlice, Node memPtr, Node value) { + super(name, memSlice, memPtr, value); + } + + @Override + public String label() { return "Store"; } + @Override + public boolean isMem() { return true; } + public Node val() { return in(3); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return sb.append(".").append(_name).append("=").append( val()).append(";"); + } + + @Override + public Type compute() { return TypeMem.BOT; } + + @Override + public Node idealize() { + + // Simple store-after-store on same address. Should pick up the + // required init-store being stomped by a first user store. + if( mem() instanceof StoreNode st && + ptr()==st.ptr() && _name.equals(st._name) && // Must check same object + ptr()._type instanceof TypeMemPtr && // No bother if weird dead pointers + // Must have exactly one use of "this" or you get weird + // non-serializable memory effects in the worse case. + st.checkNoUseBeyond(this) ) { + setDef(1,st.mem()); + return this; + } + + return null; + } + + // Check that `this` has no uses beyond `that` + private boolean checkNoUseBeyond(Node that) { + if( nOuts()==1 ) return true; + // Add deps on the other uses (can be e.g. ScopeNode mid-parse) so that + // when the other uses go away we can retry. + for( Node use : _outputs ) + if( use != that ) + use.addDep(that); + return false; + } + +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/node/SubNode.java b/chapter10a/src/main/java/com/seaofnodes/simple/node/SubNode.java new file mode 100644 index 00000000..eeec5e1d --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/node/SubNode.java @@ -0,0 +1,63 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class SubNode extends Node { + public SubNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Sub"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("-"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + // Sub of same is 0 + if( in(1)==in(2) ) + return TypeInteger.ZERO; + + if (t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1) { + if (i0.isConstant() && i1.isConstant()) + return TypeInteger.constant(i0.value()-i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Keep invalid operands for type checking. + if( t1 instanceof TypeInteger x && t2 instanceof TypeInteger y ) { + if( y.isConstant() && y.value()==0 ) return lhs; + if( x.isConstant() && x.value()==0 ) return new MinusNode(rhs); + } + + // x - (-y) is x+y + if( rhs instanceof MinusNode minus ) + return new AddNode(lhs,minus.in(1)); + + // (-x) - y is -(x+y) + if( lhs instanceof MinusNode minus ) + return new MinusNode(new AddNode(minus.in(1),rhs).peephole()); + + return null; + } + + @Override Node copy(Node lhs, Node rhs) { return new SubNode(lhs,rhs); } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter10a/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java new file mode 100644 index 00000000..5e21c852 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.graph.GraphAdapter; +import com.seaofnodes.graph.GraphSnapshot; +import com.seaofnodes.graph.GraphSnapshot.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMem; +import java.util.ArrayList; + +/** Chapter 10's view of the IR; browser, transport and layout are shared. */ +public class SimpleGraphAdapter extends GraphAdapter { + @Override protected boolean dead(Node n) { return n.isDead(); } + @Override protected int id(Node n) { return n._nid; } + @Override protected int nIns(Node n) { return n.nIns(); } + @Override protected Node in(Node n, int idx) { return n.in(idx); } + @Override protected int nOuts(Node n) { return n.nOuts(); } + @Override protected Node out(Node n, int idx) { return n.out(idx); } + @Override protected int nDeps(Node n) { return n.nDeps(); } + @Override protected Node dep(Node n, int idx) { return n.dep(idx); } + + private ArrayList edges(Node n) { + var edges = new ArrayList(); + String[] names = n instanceof ScopeNode scope && n.nIns() != 0 ? scope.reverseNames() : null; + for( int i = 0; i < n.nIns(); i++ ) { + String name = names == null || i >= names.length ? null : names[i]; + edges.add(new Edge(i, ref(n.in(i)), role(n, i), name)); + } + return edges; + } + + @Override protected GraphSnapshot.Node desc(Node n) { + Projection proj = n instanceof ProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; + String label = n.label(); + return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + } + + private Kind kind(Node n) { + if( n instanceof StopNode ) return Kind.STOP; + if( n instanceof ScopeNode ) return Kind.SCOPE; + if( n instanceof StartNode ) return Kind.CTRL; + if( n instanceof LoopNode ) return Kind.LOOP; + if( n instanceof RegionNode ) return Kind.REGION; + if( n instanceof PhiNode ) return Kind.PHI; + return n.isCFG() ? Kind.CTRL : isMem(n) ? Kind.MEM : Kind.DATA; + } + + private Role role(Node n, int i) { + if( n instanceof ScopeNode || n instanceof ConstantNode ) return Role.ASSOC; + if( n instanceof PhiNode ) return i == 0 ? Role.ASSOC : isMem(n) ? Role.MEM : Role.DATA; + if( n instanceof ProjNode ) return n.isCFG() ? Role.CTRL : isMem(n) ? Role.MEM : Role.DATA; + if( n instanceof RegionNode && i == 0 ) return Role.ASSOC; + if( i == 0 || n instanceof RegionNode || n instanceof StopNode ) return Role.CTRL; + if( i == 1 && n instanceof MemOpNode || i == 2 && n instanceof ReturnNode ) return Role.MEM; + Node def = n.in(i); + if( def != null && isMem(def) ) return Role.MEM; + return Role.DATA; + } + + private boolean isMem(Node n) { return n.isMem() || n._type instanceof TypeMem; } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter10a/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java new file mode 100644 index 00000000..1980659e --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.graph.GraphCapture; +import com.seaofnodes.graph.GraphViewer; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.Node; +import java.util.ArrayList; +import java.io.IOException; + +/** Chapter 10's compilation context; capture and event bookkeeping are shared. */ +public class SimpleGraphObserver extends GraphCapture { + private Parser _parser; + private Node _ret; + private String _phase; + public SimpleGraphObserver() { super(new SimpleGraphAdapter()); } + public static void main(String[] args) throws IOException { + GraphViewer.run(new SimpleGraphObserver()); + } + @Override protected void compile(String src) { + _parser = new Parser(src); + _parser._obs = this; + _phase = "Parse"; + _ret = _parser.parse(); + phase(_phase); + _phase = "Iter"; + com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); + phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); + } + @Override protected String phase() { return _phase; } + @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } + @Override protected void detach() { + if( _parser != null ) _parser._obs = null; + _parser = null; + _ret = null; + } + @Override protected Node scope() { return _parser._scope; } + @Override protected void roots(ArrayList roots) { + roots.add(Parser.START); + roots.add(_parser.STOP); + roots.addAll(_parser._xScopes); + roots.add(_ret); + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter10a/src/main/java/com/seaofnodes/simple/type/Field.java new file mode 100644 index 00000000..cdc58215 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/type/Field.java @@ -0,0 +1,59 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a field in a struct. This is not a Type in the type system. + */ +public class Field extends Type { + + // The pair {fieldName,type} uniquely identifies a field. + + // Field name + public final String _fname; + // Type of the field + public final Type _type; + + private Field(String fname, Type type ) { + super(TFLD); + _fname = fname; + _type = type; + } + public static Field make( String fname, Type type ) { + return new Field(fname,type).intern(); + } + + public static final Field TEST = make("test",TypeInteger.ZERO); + public static void gather(ArrayList 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/chapter10a/src/main/java/com/seaofnodes/simple/type/Type.java b/chapter10a/src/main/java/com/seaofnodes/simple/type/Type.java new file mode 100644 index 00000000..bd9cf458 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/type/Type.java @@ -0,0 +1,187 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * These types are part of a Monotone Analysis Framework, + * @see see for example this set of slides. + *

+ * The types form a lattice; @see a symmetric complete bounded (ranked) lattice. + *

+ * This wild lattice theory will be needed later to allow us to easily beef up + * the analysis and optimization of the Simple compiler... but we don't need it + * now, just know that it is coming along in a later Chapter. + *

g + * One of the fun things here is that while the theory is deep and subtle, the + * actual implementation is darn near trivial and is generally really obvious + * what we're doing with it. Right now, it's just simple integer math to do + * simple constant folding e.g. 1+2 == 3 stuff. + */ + +public class Type { + static final HashMap INTERN = new HashMap<>(); + + // ---------------------------------------------------------- + // Simple types are implemented fully here. "Simple" means: the code and + // type hierarchy are simple, not that the Type is conceptually simple. + static final byte TBOT = 0; // Bottom (ALL) + static final byte TTOP = 1; // Top (ANY) + static final byte TCTRL = 2; // Ctrl flow bottom + static final byte TXCTRL = 3; // Ctrl flow top (mini-lattice: any-xctrl-ctrl-all) + static final byte TSIMPLE = 4; // End of the Simple Types + static final byte TINT = 5; // All Integers; see TypeInteger + static final byte TTUPLE = 6; // Tuples; finite collections of unrelated Types, kept in parallel + static final byte TMEM = 7; // Whole-memory token + static final byte TMEMPTR = 8; // Memory pointer type + static final byte TSTRUCT = 9; // Structs; tuples with named fields + static final byte TFLD =10; // Fields into struct + + public final byte _type; + + public boolean is_simple() { return _type < TSIMPLE; } + private static final String[] STRS = new String[]{"Bot","Top","Ctrl","~Ctrl"}; + protected Type(byte type) { _type = type; } + + public static final Type BOTTOM = new Type( TBOT ).intern(); // ALL + 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; } + + // Strict constant values, things on the lattice centerline. + // Excludes both high and low values + public boolean isConstant() { return false; } + + /** + * 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" + public T intern() { + T nnn = (T)INTERN.get(this); + if( nnn==null ) + INTERN.put(nnn=(T)this,this); + return nnn; + } + + private int _hash; // Hash cache; not-zero when set. + @Override + public final int hashCode() { + if( _hash!=0 ) return _hash; + _hash = hash(); + if( _hash==0 ) _hash = 0xDEADBEEF; // Bad hash from subclass; use some junk thing + return _hash; + } + // Override in subclasses + int hash() { return _type; } + + @Override + public final boolean equals( Object o ) { + if( o==this ) return true; + if( !(o instanceof Type t)) return false; + if( _type != t._type ) return false; + return eq(t); + } + // Overridden in subclasses; subclass can assume "this!=t" and java classes are same + boolean eq(Type t) { return true; } + + + // ---------------------------------------------------------- + public final Type meet(Type t) { + // Shortcut for the self case + if( t == this ) return this; + // Same-type is always safe in the subclasses + if( _type==t._type ) return xmeet(t); + // 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 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()'. + 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; + if( _type==TTOP || t._type==TBOT ) return t; + // 'this' is {TCTRL,TXCTRL} + if( !t.is_simple() ) return BOTTOM; + // 't' is {TCTRL,TXCTRL} + return _type==TCTRL || t._type==TCTRL ? CONTROL : XCONTROL; + } + + public Type dual() { + return switch( _type ) { + case TBOT -> TOP; + case TTOP -> BOTTOM; + case TCTRL -> XCONTROL; + case TXCTRL -> CONTROL; + default -> throw Utils.TODO(); // Should not reach here + }; + } + + // ---------------------------------------------------------- + // 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/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java new file mode 100644 index 00000000..8c77672b --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -0,0 +1,101 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +/** + * Integer Type + */ +public class TypeInteger extends Type { + + public final static TypeInteger TOP = make(false, 0); + public final static TypeInteger BOT = make(false, 1); + public final static TypeInteger ZERO= make(true , 0); + + public final boolean _is_con; + + /** + * The constant value or + * if not constant then 1=bottom, 0=top. + */ + public final long _con; + + private TypeInteger(boolean is_con, long con) { + super(TINT); + _is_con = is_con; + _con = con; + } + public static TypeInteger make(boolean is_con, long con) { + return new TypeInteger(is_con,con).intern(); + } + + 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"); + if( this==BOT ) return sb.append("IntBot"); + 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; } + + @Override + public boolean isConstant() { return _is_con; } + + public long value() { return _con; } + + @Override + public Type xmeet(Type other) { + // Invariant from caller: 'this' != 'other' and same class (TypeInteger) + TypeInteger i = (TypeInteger)other; // Contract + // BOT wins + if ( this==BOT ) return this; + if ( i ==BOT ) return i ; + // TOP loses + if ( i ==TOP ) return this; + if ( this==TOP ) return i ; + // Since both are constants, and are never equals (contract) unequals + // constants fall to bottom + return BOT; + } + + @Override + public Type dual() { + if( isConstant() ) return this; // Constants are a self-dual + 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 + public boolean eq( Type t ) { + TypeInteger i = (TypeInteger)t; // Contract + return _con==i._con && _is_con==i._is_con; + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeMem.java new file mode 100644 index 00000000..d921bac3 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeMem.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +/** A token for all memory; field values are not tracked in its type. */ +public class TypeMem extends Type { + private final boolean _high; + private TypeMem(boolean high) { super(TMEM); _high = high; } + public static final TypeMem TOP = new TypeMem(true ).intern(); + public static final TypeMem BOT = new TypeMem(false).intern(); + + public static void gather(ArrayList ts) { ts.add(BOT); } + @Override TypeMem xmeet(Type t) { return BOT; } + @Override public Type dual() { return _high ? BOT : TOP; } + @Override public Type glb() { return BOT; } + @Override int hash() { return _high ? 1 : 2; } + @Override boolean eq(Type t) { return _high==((TypeMem)t)._high; } + @Override public StringBuilder print(StringBuilder sb) { return sb.append(_high ? "MemTop" : "MemBot"); } + @Override public String str() { return print(new StringBuilder()).toString(); } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java new file mode 100644 index 00000000..6c874a93 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java @@ -0,0 +1,82 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +/** + * Represents a Pointer to memory. + * + * Null is generic pointer to non-existent memory. + * *void is a non-Null pointer to all possible structs. + * Pointers can be to specific struct types, or a union with Null. + * The distinguished *$BOT ptr represents union of *void and Null. + * The distinguished *$TOP ptr represents the dual of *$BOT. + */ +public class TypeMemPtr extends Type { + // A TypeMemPtr is pair (obj,nil) + // where obj is one of + // (null,TypeStruct). + // where nil is one of + // (true,false) meaning an explicit null is allowed or not + + public final TypeStruct _obj; // null, a TypeStruct, or sentinel TypeStruct.MANY + public final boolean _nil; + + private TypeMemPtr(TypeStruct obj, boolean nil) { + super(TMEMPTR); + _obj = obj; + _nil = nil; + } + public static TypeMemPtr make(TypeStruct obj, boolean nil) { return new TypeMemPtr(obj, nil).intern(); } + public static TypeMemPtr make(TypeStruct obj) { return TypeMemPtr.make(obj, false); } + + public static final TypeMemPtr BOT = make(TypeStruct.BOT,true); + public static final TypeMemPtr TOP = BOT.dual(); + public static final TypeMemPtr NULLPTR = make(TypeStruct.TOP,true); + public static final TypeMemPtr VOIDPTR = NULLPTR.dual(); // A bottom mix of not-null ptrs, like C's void* but not null + public static final TypeMemPtr TEST= make(TypeStruct.TEST,false); + public static void gather(ArrayList ts) { ts.add(NULLPTR); ts.add(BOT); ts.add(TEST); } + + @Override + Type xmeet(Type t) { + TypeMemPtr that = (TypeMemPtr) t; + return TypeMemPtr.make((TypeStruct)_obj.meet(that._obj), _nil | that._nil); + } + + @Override + public TypeMemPtr dual() { return TypeMemPtr.make(_obj==null ? null : _obj.dual(), !_nil); } + + @Override + public Type glb() { + if( _obj==null ) return BOT; + return make(_obj.glb(),true); + } + @Override public TypeMemPtr makeInit() { return NULLPTR; } + + @Override + int hash() { return (_obj==null ? 0xDEADBEEF : _obj.hashCode()) ^ (_nil ? 1024 : 0); } + + @Override + boolean eq(Type t) { + TypeMemPtr ptr = (TypeMemPtr)t; // Invariant + return _obj == ptr._obj && _nil == ptr._nil; + } + + // [void,name,MANY]*[,?] + @Override + public StringBuilder print(StringBuilder sb) { + if( this==BOT ) return sb.append("PtrBot"); + if( this==TOP ) return sb.append("PtrTop"); + if( this== NULLPTR) return sb.append("null"); + if( this== VOIDPTR) return sb.append("*void"); + return _obj.print(sb.append("*")).append(_nil ? "?" : ""); + } + + @Override public String str() { + if( this==BOT ) return "PtrBot"; + if( this==TOP ) return "PtrTop"; + if( this== NULLPTR) return "null"; + if( this== VOIDPTR) return "*void"; + return "*"+_obj.str()+(_nil ? "?" : ""); + } + +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java new file mode 100644 index 00000000..44741def --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -0,0 +1,127 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a struct type. + */ +public class TypeStruct extends Type { + + // For now in terms of the lattice + // a struct type just stays as is + + public final String _name; + public final Field[] _fields; + + private TypeStruct(String name, Field[] fields) { + super(Type.TSTRUCT); + _name = name; + _fields = fields; + } + + public static TypeStruct make(String name, Field[] fields) { return new TypeStruct(name, fields).intern(); } + public static TypeStruct make(String name, ArrayList 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) { + if( this==BOT ) return sb.append("StructBot"); + if( this==TOP ) return sb.append("StructTop"); + sb.append(_name).append(" {\n"); + for( Field f : _fields ) { + sb.append(" ").append(f._fname).append(":"); + f._type.print(sb); + sb.append(";\n"); + } + return sb.append("}"); + } + + @Override public String str() { + if( this==BOT ) return "StructBot"; + if( this==TOP ) return "StructTop"; + return _name; + } +} diff --git a/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeTuple.java b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeTuple.java new file mode 100644 index 00000000..0aa019a7 --- /dev/null +++ b/chapter10a/src/main/java/com/seaofnodes/simple/type/TypeTuple.java @@ -0,0 +1,88 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +public class TypeTuple extends Type { + + public final Type[] _types; + + 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 + Type xmeet(Type other) { + TypeTuple tt = (TypeTuple)other; // contract from xmeet + assert _types.length == tt._types.length; + Type[] ts = new Type[_types.length]; + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i].meet(tt._types[i]); + return make(ts); + } + + @Override public Type dual() { + Type[] ts = new Type[_types.length]; + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i].dual(); + 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("["); + for( Type t : _types ) + t.print(sb).append(","); + sb.setLength(sb.length()-1); + sb.append("]"); + 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}); + public static final TypeTuple IF_FALSE = make(new Type[]{Type.XCONTROL,Type. CONTROL}); + + @Override + int hash() { + int sum = 0; + for( Type type : _types ) sum ^= type.hashCode(); + return sum; + } + + @Override + boolean eq( Type t ) { + TypeTuple tt = (TypeTuple)t; // Contract + if( _types.length != tt._types.length ) return false; + for( int i=0; i<_types.length; i++ ) + if( _types[i]!=tt._types[i] ) + return false; + return true; + } + + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter01Test.java new file mode 100644 index 00000000..89882b2f --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter01Test.java @@ -0,0 +1,100 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter01Test { + + @Test + public void testSimpleProgram() { + Parser parser = new Parser("return 1;"); + StopNode stop = parser.parse(); + StartNode start = Parser.START; + ReturnNode ret = (ReturnNode)stop.in(0); + + assertTrue(ret.ctrl() instanceof ProjNode); + Node expr = ret.expr(); + if( expr instanceof ConstantNode con ) { + assertEquals(start,con.in(0)); + assertEquals(TypeInteger.constant(1), con._type); + } else { + fail(); + } + } + + @Test + public void testZero() { + Parser parser = new Parser("return 0;"); + parser.parse(); + StartNode start = Parser.START; + for( Node use : start._outputs ) + if( use instanceof ConstantNode con && con._type instanceof TypeInteger ) + assertEquals(TypeInteger.constant(0),con._type); + } + + @Test + public void testBad1() { + try { + new Parser("ret").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'ret'",e.getMessage()); + } + } + + @Test + public void testBad2() { + try { + new Parser("return 0123;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error: integer values cannot start with '0'",e.getMessage()); + } + } + + @Test + public void testNotBad3() { + // this test used to fail in chapter 1 + assertEquals("return 12;", new Parser("return --12;").parse().print()); + } + + @Test + public void testBad4() { + try { + new Parser("return 100").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected ;: ",e.getMessage()); + } + } + + @Test + public void testNotBad5() { + // this test used to fail in chapter 1 + assertEquals("return -100;", new Parser("return -100;").parse().print()); + } + + @Test + public void testBad6() { + try { + new Parser("return100").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'return100'",e.getMessage()); + } + } + + @Test + public void testBad7() { + try { + new Parser("return 1;}").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, unexpected }",e.getMessage()); + } + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/Chapter02Test.java b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter02Test.java new file mode 100644 index 00000000..a6075b75 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter02Test.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter02Test { + + @Test + public void testParseGrammar() { + Parser parser = new Parser("return 1+2*3+-5;"); + StopNode ret = parser.parse(); + assertEquals("return 2;", ret.print()); + } + + @Test + public void testAddPeephole() { + Parser parser = new Parser("return 1+2;"); + StopNode ret = parser.parse(); + assertEquals("return 3;", ret.print()); + } + + @Test + public void testSubPeephole() { + Parser parser = new Parser("return 1-2;"); + StopNode ret = parser.parse(); + assertEquals("return -1;", ret.print()); + } + + @Test + public void testMulPeephole() { + Parser parser = new Parser("return 2*3;"); + StopNode ret = parser.parse(); + assertEquals("return 6;", ret.print()); + } + + @Test + public void testDivPeephole() { + Parser parser = new Parser("return 6/3;"); + StopNode ret = parser.parse(); + assertEquals("return 2;", ret.print()); + } + + @Test + public void testMinusPeephole() { + Parser parser = new Parser("return 6/-3;"); + StopNode ret = parser.parse(); + assertEquals("return -2;", ret.print()); + } + + @Test + public void testExample() { + Parser parser = new Parser("return 1+2*3+-5;"); + StopNode ret = parser.parse(); + assertEquals("return 2;", ret.print()); + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/Chapter03Test.java b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter03Test.java new file mode 100644 index 00000000..609dff76 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter03Test.java @@ -0,0 +1,69 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter03Test { + + @Test + public void testVarDecl() { + Parser parser = new Parser("int a=1; return a;"); + StopNode ret = parser.parse(); + assertEquals("return 1;", ret.print()); + } + + @Test + public void testVarAdd() { + Parser parser = new Parser("int a=1; int b=2; return a+b;"); + StopNode ret = parser.parse(); + assertEquals("return 3;", ret.print()); + } + + @Test + public void testVarScope() { + Parser parser = new Parser("int a=1; int b=2; int c=0; { int b=3; c=a+b; } return c;"); + StopNode ret = parser.parse(); + assertEquals("return 4;", ret.print()); + } + + @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); "); + StopNode ret = parser.parse(); + assertEquals("return 8;", ret.print()); + } + + @Test + public void testSelfAssign() { + try { + new Parser("int a=a; return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'a'",e.getMessage()); + } + } + + @Test + public void testRedeclareVar() { + try { + new Parser("int a=1; int b=2; int c=0; int b=3; c=a+b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Redefining name 'b'",e.getMessage()); + } + } + + @Test + public void testBad1() { + try { + new Parser("int a=1; int b=2; int c=0; { int b=3; c=a+b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected }: ",e.getMessage()); + } + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter04Test.java new file mode 100644 index 00000000..f1603cc4 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter04Test.java @@ -0,0 +1,162 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter04Test { + + @Test + public void testPeephole() { + Parser parser = new Parser("return 1+arg+2; "); + StopNode ret = parser.parse(); + assertEquals("return (arg+3);", ret.print()); + } + + @Test + public void testPeephole2() { + Parser parser = new Parser("return (1+arg)+2;"); + StopNode ret = parser.parse(); + assertEquals("return (arg+3);", ret.print()); + } + + @Test + public void testAdd0() { + Parser parser = new Parser("return 0+arg;"); + StopNode ret = parser.parse(); + assertEquals("return arg;", ret.print()); + } + + @Test + public void testAddAddMul() { + Parser parser = new Parser("return arg+0+arg;"); + StopNode ret = parser.parse(); + assertEquals("return (arg*2);", ret.print()); + } + + @Test + public void testPeephole3() { + Parser parser = new Parser("return 1+arg+2+arg+3; "); + StopNode ret = parser.parse(); + assertEquals("return ((arg*2)+6);", ret.print()); + } + + @Test + public void testMul1() { + Parser parser = new Parser("return 1*arg;"); + StopNode ret = parser.parse(); + assertEquals("return arg;", ret.print()); + } + + @Test + public void testSub0() { + Parser parser = new Parser("return arg-0;"); + StopNode ret = parser.parse(); + assertEquals("return arg;", ret.print()); + } + + @Test + public void test0Sub() { + Parser parser = new Parser("return 0-arg;"); + StopNode ret = parser.parse(); + assertEquals("return (-arg);", ret.print()); + } + + @Test + public void testSubNeg() { + Parser parser = new Parser("return 3--arg;"); + StopNode ret = parser.parse(); + assertEquals("return (arg+3);", ret.print()); + } + + @Test + public void testNegSub() { + Parser parser = new Parser("return -arg-3;"); + StopNode ret = parser.parse(); + assertEquals("return (-(arg+3));", ret.print()); + } + + @Test + public void testVarArg() { + Parser parser = new Parser("return arg; "); + StopNode stop = parser.parse(); + ReturnNode ret = stop.ret(); + assertTrue(ret.in(0) instanceof ProjNode); + assertTrue(ret.in(1) instanceof ProjNode); + } + + @Test + public void testConstantArg() { + Parser parser = new Parser("return arg; ", TypeInteger.constant(2)); + StopNode ret = parser.parse(); + assertEquals("return 2;", ret.print()); + } + + @Test + public void testCompEq() { + Parser parser = new Parser("return 3==3; "); + StopNode ret = parser.parse(); + assertEquals("return 1;", ret.print()); + } + + @Test + public void testCompEq2() { + Parser parser = new Parser("return 3==4; "); + StopNode ret = parser.parse(); + assertEquals("return 0;", ret.print()); + } + + @Test + public void testCompNEq() { + Parser parser = new Parser("return 3!=3; "); + StopNode ret = parser.parse(); + assertEquals("return 0;", ret.print()); + } + + @Test + public void testCompNEq2() { + Parser parser = new Parser("return 3!=4; "); + StopNode ret = parser.parse(); + assertEquals("return 1;", ret.print()); + } + + @Test + public void testBug1() { + Parser parser = new Parser("int a=arg+1; int b=a; b=1; return a+2; "); + StopNode ret = parser.parse(); + assertEquals("return (arg+3);", ret.print()); + } + + @Test + public void testBug2() { + Parser parser = new Parser("int a=arg+1; a=a; return a; "); + StopNode ret = parser.parse(); + assertEquals("return (arg+1);", ret.print()); + } + + @Test + public void testBug3() { + try { + new Parser("inta=1; return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'inta'",e.getMessage()); + } + } + + @Test + public void testBug4() { + Parser parser = new Parser("return -arg;"); + StopNode ret = parser.parse(); + assertEquals("return (-arg);", ret.print()); + } + + @Test + public void testBug5() { + Parser parser = new Parser("return arg--2;"); + StopNode ret = parser.parse(); + assertEquals("return (arg--2);", ret.print()); + } +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter05Test.java new file mode 100644 index 00000000..aefb3a5c --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter05Test.java @@ -0,0 +1,213 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter05Test { + + @Test + public void testIfStmt() { + Parser parser = new Parser( +""" +int a = 1; +if (arg == 1) + a = arg+2; +else { + a = arg-3; + +} + +return a;"""); + StopNode ret = parser.parse().iterate(); + assertEquals("return Phi(Region19,(arg+2),(arg-3));", ret.toString()); + } + + @Test + public void testTest() { + Parser parser = new Parser( +""" +int c = 3; +int b = 2; +if (arg == 1) { + b = 3; + c = 4; +} +return c;""", TypeInteger.BOT); + StopNode ret = parser.parse().iterate(); + assertEquals("return Phi(Region17,4,3);", ret.toString()); + } + + @Test + public void testReturn2() { + Parser parser = new Parser( +""" +if( arg==1 ) + return 3; +else + return 4; +""", TypeInteger.BOT); + StopNode stop = parser.parse(); + assertEquals("Stop[ return 3; return 4; ]", stop.toString()); + } + + @Test + public void testIfMergeB() { + Parser parser = new Parser( +""" +int a=arg+1; +int b=0; +if( arg==1 ) + b=a; +else + b=a+1; +return a+b;"""); + StopNode ret = parser.parse().iterate(); + assertEquals("return ((arg*2)+Phi(Region22,2,3));", ret.toString()); + } + + @Test + public void testIfMerge2() { + Parser parser = new Parser( +""" +int a=arg+1; +int b=arg+2; +if( arg==1 ) + b=b+a; +else + a=b+1; +return a+b;"""); + StopNode ret = parser.parse().iterate(); + assertEquals("return ((Phi(Region33,(arg*2),arg)+arg)+Phi(Region,4,5));", ret.toString()); + } + + @Test + public void testMerge3() { + Parser parser = new Parser( +""" +int a=1; +if( arg==1 ) + if( arg==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +""", TypeInteger.BOT); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Region37,Phi(Region23,2,3),Phi(Region35,4,5));", stop.toString()); + } + + @Test + public void testMerge4() { + Parser parser = new Parser( +""" +int a=0; +int b=0; +if( arg ) + a=1; +if( arg==0 ) + b=2; +return arg+a+b; +""", TypeInteger.BOT); + StopNode stop = parser.parse(); + assertEquals("return ((arg+Phi(Region14,1,0))+Phi(Region29,2,0));", stop.toString()); + } + + @Test + public void testMerge5() { + Parser parser = new Parser( +""" +int a=arg==2; +if( arg==1 ) +{ + a=arg==3; +} +return a;"""); + StopNode ret = parser.parse().iterate(); + assertEquals("return (arg==Phi(Region17,3,2));", ret.toString()); + } + + @Test + public void testTrue() { + StopNode stop = new Parser("return true;").parse(); + assertEquals("return 1;",stop.toString()); + } + + @Test + public void testHalfDef() { + try { + new Parser("if( arg==1 ) int b=2; return b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Cannot define a new name on one arm of an if",e.getMessage()); + } + } + + @Test + public void testHalfDef2() { + try { + new Parser("if( arg==1 ) { int b=2; } else { int b=3; } return b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'b'",e.getMessage()); + } + } + + @Test + public void testRegress1() { + try { + new Parser("if(arg==2) int a=1; else int b=2; return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Cannot define a new name on one arm of an if",e.getMessage()); + } + } + + + @Test + public void testBadNum() { + try { + new Parser("return 1-;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected an identifier or expression: ;",e.getMessage()); + } + } + + @Test + public void testKeyword1() { + try { + new Parser("int true=0; return true;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Expected an identifier, found 'true'",e.getMessage()); + } + } + + @Test + public void testKeyword2() { + try { + new Parser("int else=arg; if(else) else=2; else else=1; return else;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Expected an identifier, found 'else'",e.getMessage()); + } + } + + @Test + public void testKeyword3() { + try { + new Parser("int a=1; ififif(arg)inta=2;return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'ififif'",e.getMessage()); + } + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/Chapter06Test.java b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter06Test.java new file mode 100644 index 00000000..920e26e3 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter06Test.java @@ -0,0 +1,294 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter06Test { + + @Test + public void testPeepholeReturn() { + Parser parser = new Parser( +""" +if( true ) return 2; +return 1; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 2;", stop.toString()); + assertTrue(stop.ret().ctrl() instanceof ProjNode); + } + + @Test + public void testPeepholeRotate() { + Parser parser = new Parser( +""" +int a = 1; +if (arg) + a = 2; +return (arg < a) < 3; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return ((arg 4 ) + break; +} +return arg; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return arg;", stop.toString()); + } + + @Test + public void testRegress2() { + Parser parser = new Parser("if(1) return 0; else while(arg>--arg) arg=arg+1; return 0;"); + StopNode stop = parser.parse().iterate(); + assertEquals("return 0;", stop.toString()); + } + + @Test + public void testBreakOutsideLoop() { + try { + new Parser(""" +if(arg <= 10) { + break; + arg = arg + 1; +} +return arg; +""").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("No active loop for a break or continue",e.getMessage()); + } + } + + @Test + public void testRegress3() { + Parser parser = new Parser(""" +while(arg < 10) { + break; +} +return arg; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return arg;", stop.toString()); + } + + @Test + public void testRegress4() { + Parser parser = new Parser(""" +int a = 1; +while(arg < 10) { + a = a + 1; + if (arg > 2) { + int a = 17; + break; + } +} +return a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Region29,Phi(Loop8,1,(Phi_a+1)),Add);", stop.toString()); + assertTrue(stop.ret().ctrl() instanceof RegionNode); + } + + @Test + public void testRegress5() { + Parser parser = new Parser(""" +int a = 1; +while(1) { + a = a + 1; + if (a<10) continue; + break; +} +return a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return (Phi(Loop8,1,Add)+1);", stop.toString()); + assertTrue(stop.ret().ctrl() instanceof ProjNode prj && prj._idx==1 ); + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter09Test.java new file mode 100644 index 00000000..1b1920e8 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter09Test.java @@ -0,0 +1,301 @@ +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; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.Ignore; + +import static org.junit.Assert.*; + +public class Chapter09Test { + + @Test + @Ignore + public void testJig() { + Parser parser = new Parser(""" +int v0=0; +arg=0; +while(v0) { + while(1) if(arg*arg*0==0) {} + while(0) {} + arg=1; +} +return 0; + """); + StopNode stop = parser.parse().iterate(); + } + + @Test + public void testGVN1() { + Parser parser = new Parser( + """ +int x = arg + arg; +if(arg < 10) { + return arg + arg; +} +else { + x = x + 1; +} +return x; + """); + StopNode stop = parser.parse(); + assertEquals("Stop[ return (arg*2); return (Mul+1); ]", stop.toString()); + Assert.assertEquals(2L, Evaluator.evaluate(stop, 1)); + Assert.assertEquals(23L, Evaluator.evaluate(stop, 11)); + } + + @Test + public void testGVN2() { + Parser parser = new Parser( + """ +return arg*arg-arg*arg; + """); + StopNode stop = parser.parse(); + assertEquals("return 0;", stop.toString()); + Assert.assertEquals(0L, Evaluator.evaluate(stop, 1)); + } + + @Test + public void testWorklist1() { + Parser parser = new Parser( + """ +int step = 1; +while (arg < 10) { + arg = arg + step + 1; +} +return arg; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop8,arg,(Phi_arg+2));", stop.toString()); + Assert.assertEquals(11L, Evaluator.evaluate(stop, 1)); + } + + @Test + public void testWorklist2() { + Parser parser = new Parser( + """ +int cond = 0; +int one = 1; +while (arg < 10) { + if (cond) one = 2; + arg = arg + one*3 + 1; +} +return arg; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop9,arg,(Phi_arg+4));", stop.toString()); + Assert.assertEquals(13L, Evaluator.evaluate(stop, 1)); + } + + @Test + public void testWorklist3() { + Parser parser = new Parser( + """ +int v1 = 0; +int v2 = 0; +int v3 = 0; +int v4 = 0; +int v5 = 0; +int v6 = 0; +int v7 = 0; +int v8 = 0; +while (arg) { + if (v1) v2 = 1; + if (v2) v3 = 1; + if (v3) v4 = 1; + if (v4) v5 = 1; + if (v5) v6 = 1; + if (v6) v7 = 1; + if (v7) v8 = 1; + arg = arg + v8 + 1; +} +return arg; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop15,arg,(Phi_arg+1));", stop.toString()); + } + + @Test + public void testRegionPeepBug() { + Parser parser = new Parser( + """ +int v0=0; +int v1=0; +while(v1+arg) { + arg=0; + int v2=v0; + while(arg+1) {} + v0=1; + v1=v2; +} + """); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testWhile0() { + Parser parser = new Parser("while(0) continue; if(0) arg=0;"); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testWhile1() { + Parser parser = new Parser(""" +if(0) while(0) { + int arg=arg; + while(0) {} +} +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + + @Test + public void testPrecedence() { + Parser parser = new Parser("return 3-1+2;"); + StopNode stop = parser.parse().iterate(); + assertEquals("return 4;", stop.toString()); + } + + @Test + public void testSwap2() { + Parser parser = new Parser("return 1+(1+1);"); + StopNode stop = parser.parse().iterate(); + assertEquals("return 3;", stop.toString()); + } + + + @Test + public void testFuzz0() { + Parser parser = new Parser(""" +int one = 1; +int a = 0; +int zero = 0; +while(arg) { + a = -(one + a + 2); + arg = arg + 1; + one = one + zero; +} +return a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop10,0,(-(Phi_a+3)));", stop.toString()); + } + + @Test + public void testFuzz1() { + Parser parser = new Parser(""" +while(1) {} +while(arg) break; +while(arg) arg=0; +arg=0; +int v0=0!=0<-0; +return -0+0+0; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + + @Test + public void testFuzz2() { + Parser parser = new Parser("return 0+-0;"); + StopNode stop = parser.parse().iterate(); + assertEquals("return 0;", stop.toString()); + } + + @Test + public void testFuzz3() { + Parser parser = new Parser("int v0=0; while(0==69) while(v0) return 0;"); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz4() { + Parser parser = new Parser(""" +while(1) { + arg=0<=0; + if(1<0) while(arg==-0) arg=arg-arg; +} +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz5() { + Parser parser = new Parser(""" +{ + int v0=0; + while(1) + int v1=0--0; + while(v0) + break; + while(-v0) { + while(0+0+v0) continue; + break; + } + if(-0!=-0+0+v0) while(0+0+0+0) + break; +} +return 0!=0; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz6() { + Parser parser = new Parser(""" +int v0=0; +while(0==1) while(v0) + v0=1+v0; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz7() { + Parser parser = new Parser(""" +while(1) {} +int v0=0; +while(v0) + {} +int v1=0; +while(1) + v1=1; +return v1+v0; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz8() { + Parser parser = new Parser("while(arg) arg = arg - 1; return arg;"); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop7,arg,(Phi_arg-1));", stop.toString()); + } + + @Test + public void testMeet() { + Type t1 = Type.TOP; + Type t2 = TypeInteger.TOP; + Assert.assertEquals(TypeInteger.TOP, t1.meet(t2)); + Assert.assertEquals(TypeInteger.TOP, t2.meet(t1)); + t1 = Type.BOTTOM; + t2 = TypeInteger.BOT; + Assert.assertEquals(Type.BOTTOM, t1.meet(t2)); + Assert.assertEquals(Type.BOTTOM, t2.meet(t1)); + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter10Test.java new file mode 100644 index 00000000..26bbb62a --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -0,0 +1,522 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; +import com.seaofnodes.simple.type.*; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter10Test { + @Test public void testDeadReferenceReturn() { + for( String body : new String[] { + "return new S; return 0;", + "if(1) return new S; else return 0;" + } ) { + String src = "struct S { int x; }; " + body; + var stop = new Parser(src).parse().iterate(); + org.junit.Assert.assertTrue(body, + stop.ret().expr()._type instanceof com.seaofnodes.simple.type.TypeMemPtr); + } + } + + + + // Issue #246: null-check guards start in Chapter 10; arrays arrive in Chapter 15. + private static final String NULLABLE_POINT_SOURCE = """ + struct Point { int x; }; + Point point = new Point; + point.x = 42; + Point? p = null; + if (arg) p = point; + """; + + @Test + public void testNullGuards() { + for( String body : new String[] { + "if (p != null) return p.x; return -1;", + "if (null != p) return p.x; return -1;", + "if (!!!!p) return p.x; return -1;", + "if (!!!p) return -1; else return p.x;", + "int b = !!p; if (b) return p.x + b - 1; return -1;" + } ) { + StopNode stop = new Parser(NULLABLE_POINT_SOURCE+body).parse().iterate(); + assertEquals(body,-1L,com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,0)); + assertEquals(body,42L,com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,1)); + } + } + + @Test + public void testNullGuardErrors() { + for( String body : new String[] { + "return p.x;", + "if (p == null) return p.x; return -1;", + "if (!!point) return p.x; return -1;", + "if (!!p) { int x = p.x; } return p.x;" + } ) { + try { + new Parser(NULLABLE_POINT_SOURCE+body).parse().iterate(); + fail(body); + } catch( RuntimeException e ) { + // Known null is rejected during parsing in these chapters. + String expected = body.startsWith("if (p == null)") + ? "Accessing unknown field 'x' from 'null'" : "Might be null accessing 'x'"; + assertEquals(body,expected,e.getMessage()); + } + } + } + + @Test + public void testFuzzer() { + Parser parser = new Parser( +""" +int a = arg/3; +int b = arg*5; +int x = arg*7; +int y = arg/11; +int p; int g; int h; +if( (arg/13)==0 ) { + p = x + y; + g = x; + h = y; +} else { + p = a + b; + g = a; + h = b; +} +int r = g+h; +return p-r; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 0;", stop.toString()); + } + + @Test + public void testStruct() { + Parser parser = new Parser(""" +struct Bar { + int a; + int b; +} +struct Foo { + int x; +} +Foo? foo = null; +Bar bar = new Bar; +bar.a = 1; +bar.a = 2; +return bar.a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 2;", stop.toString()); + } + + @Test + public void testExample() { + Parser parser = new Parser(""" +struct Vector2D { int x; int y; } +Vector2D v = new Vector2D; +v.x = 1; +if (arg) + v.y = 2; +else + v.y = 3; +return v; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return new Vector2D;", stop.toString()); + } + + @Test + public void testBug() { + Parser parser = new Parser(""" +struct s0 { + int v0; +} +s0? v1=null; +int v3=v1.zAicm; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'zAicm' from 'null'",e.getMessage()); } + } + + @Test + public void testBug2() { + Parser parser = new Parser(""" +struct s0 { int v0; } +arg=0+new s0.0; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Expected an identifier, found 'null'",e.getMessage()); } + } + + @Test + public void testLoop() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar bar = new Bar; +while (arg) { + bar.a = bar.a + 2; + arg = arg + 1; +} +return bar.a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop10,0,(Phi_a+2));", stop.toString()); + } + + @Test + public void testIf() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar bar = new Bar; +if (arg) bar = null; +bar.a = 1; +return bar.a; +"""); + try { parser.parse().iterate(); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Bar",e.getMessage()); } + } + + @Test + public void testIf2() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar? bar = null; +if (arg) bar = new Bar; +bar.a = 1; +return bar.a; +"""); + try { parser.parse().iterate(); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'a'",e.getMessage()); } + } + + @Test + public void testIf3() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar bar = null; +if (arg) bar = null; +bar.a = 1; +return bar.a; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Bar", e.getMessage()); } + } + + @Test + public void testIfOrNull() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar? bar = new Bar; +if (arg) bar = null; +if( bar ) bar.a = 1; +return bar; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Region15,null,new Bar);", stop.toString()); + } + + @Test + public void testIfOrNull2() { + Parser parser = new Parser( +""" +struct Bar { int a; } +Bar? bar = new Bar; +if (arg) bar = null; +int rez = 3; +if( !bar ) rez=4; +else bar.a = 1; +return rez; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Region32,4,3);", stop.toString()); + } + + @Test + public void testWhileWithNullInside() { + Parser parser = new Parser(""" +struct s0 {int v0;} +s0? v0 = new s0; +int ret = 0; +while(arg) { + ret = v0.v0; + v0 = null; + arg = arg - 1; +} +return ret; +"""); + try { parser.parse().iterate(); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'v0'", e.getMessage()); } + } + + @Test + public void testRedeclareStruct() { + Parser parser = new Parser(""" +struct s0 { + int v0; +} +s0? v1=new s0; +s0? v1; +v1=new s0; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Redefining name 'v1'", e.getMessage()); } + } + + @Test + public void testIter() { + // Build and use an iterator + Parser parser = new Parser( +""" +struct Iter { + int x; + int len; +} +Iter i = new Iter; +i.len = arg; +int sum=0; +while( i.x < i.len ) { + sum = sum + i.x; + i.x = i.x + 1; +} +return sum; +"""); + StopNode stop = parser.parse().iterate(); + // The preceding len store keeps the initial x load on the bulk chain. + assertEquals("return Phi(Loop13,0,(Phi(Loop,.x,(Phi_x+1))+Phi_sum));", stop.toString()); + assertEquals(10L, com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,5)); + } + + + + + @Test + public void test1() { + Parser parser = new Parser(""" +struct s0 {int v0;} +s0 ret = new s0; +while(arg) { + s0 v0 = new s0; + v0.v0 = arg; + arg = arg-1; + if (arg==5) ret=v0; +} +return ret; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop10,new s0,Phi(Region31,new s0,Phi_ret));", stop.toString()); + } + + @Test + public void test2() { + Parser parser = new Parser(""" +struct s0 {int v0;} +s0 ret = new s0; +s0 v0 = new s0; +while(arg) { + v0.v0 = arg; + arg = arg-1; + if (arg==5) ret=v0; +} +return ret; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop13,new s0,Phi(Region32,new s0,Phi_ret));", stop.toString()); + } + + + @Test + public void test3() { + Parser parser = new Parser(""" +struct s0 {int v0;} +s0 ret = new s0; +while(arg < 10) { + s0 v0 = new s0; + if (arg == 5) ret=v0; + arg = arg + 1; +} +return ret; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop10,new s0,Phi(Region30,new s0,Phi_ret));", stop.toString()); + } + + @Test + public void testBug3() { + Parser parser = new Parser(""" +struct s0 { + int f0; +} +if(0>=0) return new s0; +return new s0; +int v0=null.f0; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'f0' from 'null'", e.getMessage()); } + } + + @Test + public void testBug4() { + Parser parser = new Parser(""" +if(0) { + while(0) if(arg) continue; + int v0=0; + while(1) { + int arg=-arg; + v0=arg; + } +} + """); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testBug5() { + Parser parser = new Parser(""" +struct s0 { + int f0; +} +if(0) return 0; +else return new s0; +if(new s0.f0) return 0; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return new s0;", stop.toString()); + } + + @Test + public void testBug6MissedWorklist() { + Parser parser = new Parser(""" +while(0) {} +int v4=0; +while(0 0) { + s.x = s.x + 1; + int j = 3; + while (j > 0) { + j = j - 1; + if (j == 1) continue; + s.y = s.y + arg; + if (arg == 2) break; + s.z = s.z + 1; + } + arg = arg - 1; + } + return s; + """).parse().iterate(); + for (int arg=0; arg<7; arg++) { + var obj = (com.seaofnodes.simple.evaluator.Evaluator.Obj) + com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,arg); + org.junit.Assert.assertArrayEquals(new Object[] { + 5L+arg, 7L+arg*(arg+1)-(arg>=2 ? 2 : 0), 11L+2*arg-(arg>=2 ? 2 : 0) + }, obj.fields()); + } + } + + @Test public void testMemoryAtEarlyReturns() { + var stop = new Parser(""" + struct S { int x; int y; int z; } + S s = new S; + s.x = 3; s.y = 5; s.z = 7; + if (arg) { s.x = 11; s.z = 13; return s; } + s.y = 17; + return s; + """).parse().iterate(); + for (int arg=0; arg<2; arg++) { + var obj = (com.seaofnodes.simple.evaluator.Evaluator.Obj) + com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,arg); + org.junit.Assert.assertArrayEquals(arg==0 ? new Object[]{3L,17L,7L} : new Object[]{11L,5L,13L}, obj.fields()); + } + } +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/FuzzerWrap.java b/chapter10a/src/test/java/com/seaofnodes/simple/FuzzerWrap.java new file mode 100644 index 00000000..019140ad --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/FuzzerWrap.java @@ -0,0 +1,50 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.fuzzer.Fuzzer; +import org.junit.Test; +import org.junit.Ignore; + + +import static org.junit.Assert.assertTrue; + +/** + * Normal tests run chapter-local regression seeds. Open failures and exploratory + * fuzzing are opt-in (run directly in the IDE or temporarily remove Ignore). + * Seeds depend on this chapter's generator; do not copy them between chapters. + */ +public class FuzzerWrap { + + private static final long[] REGRESSION_SEEDS = { + }; + + private static final long[] OPEN_FAILING_SEEDS = { + }; + + @Test public void fuzzPeepsRegression () { fuzzPeepsSeeds(REGRESSION_SEEDS); } + @Test @Ignore public void fuzzPeepsOpenFailures() { fuzzPeepsSeeds(OPEN_FAILING_SEEDS); } + + private static void fuzzPeepsSeeds(long... seeds) { + var fuzzer = new Fuzzer(); + for (long seed : seeds) + fuzzer.fuzzPeepsRegression(seed); + } + + @Test + @Ignore + public void fuzzPeepsLarge() { + var fuzzer = new Fuzzer(); + for (int i=0; i<1000000; i++) + fuzzer.fuzzPeeps(i); + assertTrue(fuzzer.noExceptions()); + } + + + @Test + @Ignore + public void fuzzPeepTiming() { + var fuzzer = new Fuzzer(); + int max_nid=0; + for (int i=0; i<1000000; i++) + max_nid = fuzzer.fuzzPeepTiming(i, max_nid); + } +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/SchedulerTest.java b/chapter10a/src/test/java/com/seaofnodes/simple/SchedulerTest.java new file mode 100644 index 00000000..ad595822 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/SchedulerTest.java @@ -0,0 +1,73 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.evaluator.Evaluator; +import com.seaofnodes.simple.node.StopNode; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SchedulerTest { + + private static void assertObj(Object obj, String name, Object... fields) { + assertEquals(Evaluator.Obj.class, obj.getClass()); + var o = (Evaluator.Obj)obj; + assertEquals(name, o.struct()._name); + Assert.assertArrayEquals(fields, o.fields()); + } + + @Test + @Ignore + public void testJig() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v=new S; +v.f = 2; +int i=v.f; +if (arg) v.f=1; +return i; +"""); + StopNode stop = parser.parse();//.iterate(); + var eval = new Evaluator(stop); + assertEquals(2L, eval.evaluate(1, 10)); + } + + @Test + public void testStoreInIf() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v0=new S; +if(arg) v0.f=1; +return v0; +"""); + StopNode stop = parser.parse().iterate(); + var eval = new Evaluator(stop); + assertObj(eval.evaluate(0, 10), "S", 0L); + assertObj(eval.evaluate(1, 10), "S", 1L); + } + + @Test + public void testStoreInIf2() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v=new S; +v.f = 2; +int i=new S.f; +i=v.f; +if (arg) v.f=1; +return i; +"""); + StopNode stop = parser.parse().iterate(); + var eval = new Evaluator(stop); + assertEquals(2L, eval.evaluate(0, 10)); + assertEquals(2L, eval.evaluate(1, 10)); + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/TypeTest.java b/chapter10a/src/test/java/com/seaofnodes/simple/TypeTest.java new file mode 100644 index 00000000..c9eb2747 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -0,0 +1,132 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.type.*; +import org.junit.Assert; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class TypeTest { + + // Test basic properties and GLB + @Test + public void testTypeAdHoc() { + TypeStruct s1 = TypeStruct.make("s1", new Field[]{ + Field.make("a", TypeInteger.BOT), + Field.make("b", TypeInteger.BOT) }); + TypeStruct s2 = TypeStruct.make("s2", new Field[]{ + Field.make("a", TypeInteger.BOT), + Field.make("b", TypeInteger.BOT) }); + Assert.assertEquals(s1, s1.glb()); + Assert.assertNotEquals(s1, s1.dual()); + Assert.assertEquals(s1, s1.dual().glb()); + + assertEquals(TypeStruct.BOT, s1.meet(s2)); + assertEquals(TypeMem.BOT, TypeMem.TOP.meet(TypeMem.BOT)); + assertEquals(TypeMem.TOP, TypeMem.BOT.dual()); + assertEquals(TypeMem.BOT, TypeMem.TOP.dual()); + + TypeMemPtr ptr1 = TypeMemPtr.make(s1); + Assert.assertEquals(s1, ptr1._obj); + TypeMemPtr ptr2 = TypeMemPtr.make(s2); + Assert.assertEquals(s2, ptr2._obj); + + TypeMemPtr ptr1nil = TypeMemPtr.make(s1, true); + Assert.assertEquals(s1, ptr1nil._obj); + Assert.assertTrue(ptr1nil._nil); + Assert.assertFalse(ptr1nil._obj==null); + TypeMemPtr ptr2nil = TypeMemPtr.make(s2, true); + Assert.assertEquals(s2, ptr2nil._obj); + + Assert.assertNotEquals(ptr1, ptr2); + Assert.assertNotEquals(ptr1, ptr1.glb()); + Assert.assertEquals(ptr1nil, ptr1.glb()); + + Assert.assertEquals(ptr1, ptr1.dual().dual()); + Assert.assertEquals(ptr1.glb(), ptr1.dual().glb()); + Assert.assertEquals(TypeMemPtr.BOT, ptr1.meet(ptr2nil)); + Assert.assertEquals(ptr1.glb(), ptr1.meet(TypeMemPtr.NULLPTR)); + + TypeMemPtr TOP = TypeMemPtr.TOP; + TypeMemPtr BOT = TypeMemPtr.BOT; + TypeMemPtr PTR = TypeMemPtr.VOIDPTR; + TypeMemPtr NULL = TypeMemPtr.NULLPTR; + Type PTR_meet_NULL = NULL.meet(PTR); + Assert.assertEquals(BOT, PTR_meet_NULL); + Type ptr1_meet_ptr2 = ptr1.meet(ptr2); + Assert.assertEquals(PTR, ptr1_meet_ptr2); + Type NULL_join_ptr1 = NULL.join(ptr1); + Assert.assertEquals(TOP, NULL_join_ptr1); + Type NULL_join_PTR = PTR.join(NULL); + Assert.assertEquals(TOP, NULL_join_PTR); + Type ptr1_dual = ptr1.dual(); + Type nullableptr1_dual = ptr1nil.dual(); + } + + // Test theoretical properties. + // This is a symmetric complete bounded (ranked) lattice. + // Also the meet is commutative and associative. + // The lattice has a dual (symmetric), and join is ~(~x meet ~y). + // See https://en.wikipedia.org/wiki/Lattice_(order). + @Test + public void testLatticeTheory() { + Type[] ts = Type.gather(); + + // Confirm commutative & complete + for( Type t0 : ts ) + for( Type t1 : ts ) { + check_commute (t0,t1); + check_symmetric(t0,t1); + } + + // Confirm associative + for( Type t0 : ts ) + for( Type t1 : ts ) + for( Type t2 : ts ) + assoc(t0,t1,t2); + + // Confirm symmetry. If A isa B, then A.join(C) isa B.join(C) + for( Type t0 : ts ) + for( Type t1 : ts ) + if( t0.isa(t1) ) + for( Type t2 : ts ) { + Type t02 = t0.join(t2); + Type t12 = t1.join(t2); + Type mt = t02.meet(t12); + assertSame(mt,t12); + } + } + + // By design in meet, args are already flipped to order _type, which forces + // symmetry for things with badly ordered _type fields. The question is + // still interesting for other orders. + private static void check_commute( Type t0, Type t1 ) { + if( t0==t1 ) return; + if( t0.is_simple() && !t1.is_simple() ) return; // By design, flipped the only allowed order + Type mta = t0.meet(t1); + Type mtb = t1.meet(t0); // Reverse args and try again + assertSame(mta,mtb); + } + + // A & B = MT + // Expect: ~A & ~MT == ~A + // Expect: ~B & ~MT == ~B + private static void check_symmetric( Type t0, Type t1 ) { + if( t1==t0 ) return; + Type mt = t0.meet(t1); + Type ta = mt.dual().meet(t1.dual()); + Type tb = mt.dual().meet(t0.dual()); + assertSame(ta,t1.dual()); + assertSame(tb,t0.dual()); + } + + private static void assoc( Type t0, Type t1, Type t2 ) { + Type t01 = t0 .meet(t1 ); + Type t12 = t1 .meet(t2 ); + Type t01_2 = t01.meet(t2 ); + Type t0_12 = t0 .meet(t12); + assertSame(t01_2,t0_12); + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java b/chapter10a/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java new file mode 100644 index 00000000..eaeb061b --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java @@ -0,0 +1,209 @@ +package com.seaofnodes.simple.evaluator; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Field; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.type.TypeStruct; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Objects; + +public class Evaluator { + + private static final Object MEMORY = new Object(); + + public record Obj(TypeStruct struct, Object[] fields) { + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("Obj<").append(struct._name).append("> {"); + for(int i=0; i= 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] = MEMORY; + s[2] = parameter; + values[start._nid] = s; + int i=0; + Scheduler.Block block = this.startBlock; + for(;;) { + for(; 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/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java new file mode 100644 index 00000000..7dcb88e5 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -0,0 +1,199 @@ +package com.seaofnodes.simple.fuzzer; + +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; + +/** + * Implementation of the fuzzer. + * Random testcases are generated using the ScriptGenerator. + * This will generate a script similar to how the parser parses it but instead of parsing it will + * randomly choose a variation the parser would parse and generate it. + * These scripts are the parsed by the parser and all exceptions are caught and filtered + * to only show one occurrence of one problem. This includes script which generate different + * results when graphs optimized with different worklist seeds are executed. + * To aid debugging scripts that cause errors are then reduced by applying rules and checking + * that the same issue persists. + */ +public class Fuzzer { + + private static final int EVAL_TIMEOUT = 1000; + + /** + * List of exceptions already encountered. + * This is used to filter new exceptions and discard them if they are already found. + */ + private final ArrayList exceptions = new ArrayList<>(); + + /** + * Filter and record an exception in a script. + * @param e The exception caused by the script + * @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, 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 ========="); + System.out.println(Reducer.reduce(script, e, reproducer)); + System.out.println("==========================="); + 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), seed); + } + } + + /** + * Run one test with the given seed, and check for peephole counts. + * @param seed The seed to use for generating this test case + */ + public int fuzzPeepTiming(long seed, int max_nids) { + var rand = new Random(seed); + var sb = new StringBuilder(); + new ScriptGenerator(rand, sb, true).genProgram(); + try { + var parser = new Parser(sb.toString()); + + var stop = parser.parse(); + int parse_peeps= Node.ITER_CNT; + int parse_nops = Node.ITER_NOP_CNT; + double parse_nop_ratio = (double)parse_nops/parse_peeps; + int nids = Node.UID(); + if( nids <= max_nids ) return max_nids; + + double parse_peeps_per_node = (double)parse_peeps/nids; + + stop.iterate(); + int iter_peeps= Node.ITER_CNT; + int iter_nops = Node.ITER_NOP_CNT; + double iter_nop_ratio = (double)iter_nops/iter_peeps; + double iter_peeps_per_node = (double)iter_peeps/nids; + + System.out.printf("%6d | Parsing: peeps: %5d, nops: %5d, ratio: %5.3f, last UID: %5d ps/node: %5.3f | Iter: peeps: %6d, nops: %6d, ratio: %5.3f, ps/node: %5.3f\n", + seed, + parse_peeps,parse_nops,parse_nop_ratio,nids,parse_peeps_per_node, + iter_peeps , iter_nops, iter_nop_ratio, iter_peeps_per_node); + return nids; + } catch (Throwable e) { + // ignore exceptions, let the normal fullPeeps catch and reduce, + // we're interested in valid program optimizations + } + return max_nids; + } + + + /** + * Run one test with the given seed. + * @param seed The seed to use for generating this test case + */ + public void fuzzPeeps(long seed) { + var rand = new Random(seed); + var sb = new StringBuilder(); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); + check(sb.toString(), valid, seed); + } + + + /** + * Check that no exceptions happened. + * @return true if no exception happened + */ + public boolean noExceptions() { + return exceptions.isEmpty(); + } + + /** Run a fixed chapter-local seed without reducing or swallowing failures. */ + public void fuzzPeepsRegression(long seed) { + var rand = new Random(seed); + var sb = new StringBuilder(); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); + try { + runCheck(sb.toString(), valid); + } catch (Throwable e) { + AssertionError ae = new AssertionError("Fuzzer regression seed failed: " + seed); + ae.initCause(e); + throw ae; + } + } +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java new file mode 100644 index 00000000..f947a234 --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -0,0 +1,142 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.util.BitSet; + +/** + * Some utilities for the fuzzer. + */ +class FuzzerUtils { + + /** + * Get the val of a private field + */ + @SuppressWarnings("unchecked") + public static T getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { + return (T) (obj instanceof Class clz ? getField(clz, name).get(null) : getField(obj.getClass(), name).get(obj)); + } + + /** + * Get access to a private field + */ + public static Field getField(Class clz, String name) throws NoSuchFieldException { + var field = clz.getDeclaredField(name); + try { + field.setAccessible(true); + } catch (Throwable e) { + // Intentionally left empty + } + return field; + } + + /** + * Try to rethrow an exception or create a runtime exception and throw it. + */ + public static RuntimeException rethrow(Throwable e) { + if (e instanceof RuntimeException re) return re; + if (e instanceof Error er) throw er; + return new RuntimeException(e); + } + + /** + * Copy of Node.WVISIST + * Used to clear as exceptions might happen in the walk and left this bitset not cleared + */ + private static final BitSet NodeWalkVisit; + private static final java.util.Random WorkRandom; + + /** + * Write access to Iterate.MID_ASSERT + * Exception could happen mid assert and left this at true. Used to reset to false prior to parsing. + */ + private static final MethodHandle set_MID_ASSERT; + + /** + * Some problems might trigger debug output. Suppress this output with a null print stream during fuzzing. + */ + public static final PrintStream NULL_PRINT_STREAM = new PrintStream(OutputStream.nullOutputStream()); + + static { + try { + NodeWalkVisit = getFieldValue(Node.class, "WVISIT"); + WorkRandom = getFieldValue(getFieldValue(IterPeeps.class, "WORK"), "_R"); + set_MID_ASSERT = MethodHandles.lookup().unreflectSetter(getField( IterPeeps.class, "MID_ASSERT")); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Check that the messages are similar. + */ + 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); + } + + /** + * Check if both stack traces have the same first entry. Empty stack traces can happen with NPE optimization + * and allow the other to be any stack trace. + */ + private static boolean sameStackTraceOrigin(StackTraceElement[] a, StackTraceElement[] b) { + if (a == null || a.length == 0 || b == null || b.length == 0) return true; // One or both stack traces are missing. + return a[0].equals(b[0]); + } + + /** + * Checks if two exceptions are caused by the same problem. + */ + 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 (!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 + if (!isExceptionFromSameCause(a.getCause(), b.getCause())) return false; // Different causes + var o1 = a.getSuppressed(); + var o2 = b.getSuppressed(); + if (o1 == null || o2 == null || o1.length != o2.length) return o1 == o2; // Different suppression + for (int i=0; i {} + + /** + * Holder for the rewrite rules + */ + 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("-"), ""), + new Replace(Pattern.compile("\\b\\+-"), "-"), + new Replace(Pattern.compile("(?=])(?:-|\\*|/|>=|<=|>|<|!=|==)(?![+\\-*/<>=])"), "+"), + 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"), + 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+ \\{[^{}]*}"), "") + }; + + /** + * Predicate to test if the reduced script generates the same error. + */ + private final Predicate tester; + + /** + * Create a reducer with a test function. + * @param tester Test function which returns true when the same issue is found in the script. + */ + private Reducer(Predicate tester) { + this.tester = tester; + } + + /** + * Rewrites the script with one rewrite rule. + */ + private String doReplacement(String script, Pattern pattern, Replacer[] replacements) { + var matcher = pattern.matcher(script); + var sb = new StringBuilder(); + 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(); + 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); + } + last = matcher.start(); + skip = 1; + } + sb.append(script, last, script.length()); + return sb.toString(); + } + + /** + * Clean up variable names from the random ones generated by the fuzzer. + */ + private String cleanVariables(String script) { + var matcher = Pattern.compile("\\b([0-9a-zA-Z_]+) ([0-9a-zA-Z_]+)").matcher(script); + int[] num = {0, 0}; + while (matcher.find()) { + 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; + } + } + return script; + } + + /** + * Run all rewrite rules once. + */ + private String doAllReplacements(String script) { + for (var replace: REPLACEMENTS) { + String old; + do { + old = script; + script = doReplacement(script, replace.pattern, replace.replacements); + } while (replace.iterate && !old.equals(script)); + } + return script; + } + + /** + * Reduce the script to a minimal version which produces the same problem with a test function. + * @param tester Test function which returns true when the same issue is found in the script. + */ + public static String reduce(String script, Predicate tester) { + var reducer = new Reducer(tester); + String old; + do { + old = script; + script = reducer.doAllReplacements(script); + script = reducer.cleanVariables(script); + } while (!script.equals(old)); + return script; + } + + /** + * Reduce the script to a minimal version which produces the same problem with a reproducer. + * @param ex The exception originally caused by the script + * @param reproducer Test function which throws an exception which will be checked against ex. + */ + public static String reduce(String script, Throwable ex, Consumer reproducer) { + return reduce(script, s->{ + Throwable nex = null; + try { + reproducer.accept(s); + } catch (Throwable e) { + nex = e; + } + return FuzzerUtils.isExceptionFromSameCause(nex, ex); + }); + } + +} diff --git a/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java new file mode 100644 index 00000000..155063ab --- /dev/null +++ b/chapter10a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -0,0 +1,792 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.Parser; +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. + * This generator will generate the same script for two random number generators initialized with the same seed. + * To generate valid code it implements the parser but instead of parsing it emits the statements and expressions. + * It is guaranteed to terminate. + */ +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 + */ + private static final int INDENTATION = 4; + /** + * Valid characters for identifiers. The last 10 are not valid for the first character. + */ + private static final String VAR_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + /** + * List of keywords not valid for identifiers. + */ + private static final HashSet KEYWORDS; + + static { + try { + KEYWORDS = FuzzerUtils.getFieldValue(new Parser(""), "KEYWORDS"); + } catch (Throwable e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Flag for a statement that it does not pass on control flow to the next statement. + */ + private static final int FLAG_STOP = 0x01; + /** + * Flag for a statement that it contains a final if statement without an else branch. + */ + private static final int FLAG_IF_WITHOUT_ELSE = 0x02; + /** + * Binary operators. + */ + private static final String[] BINARY_OP = {"==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/"}; + /** + * Unary operators. + */ + 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. + */ + private final Random random; + /** + * Indentation depth. + */ + private int indentation = 0; + /** + * Output string builder + */ + private final StringBuilder sb; + /** + * Number of nested loops + */ + private int loopDepth = 0; + /** + * Current scope start in the variables list. + */ + private int currScopeStart = 0; + /** + * The depth of blocks allowed. + */ + private int depth = MAX_BLOCK_DEPTH; + /** + * The depth of expressions allowed. + */ + private int exprDepth = MAX_EXPRESSION_DEPTH; + /** + * Current variables in scope. + */ + 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 boolean generateValid; + /** + * If this script might be invalid. + */ + private boolean maybeInvalid = false; + + + /** + * + * @param random the random number generator. + * @param sb the script will be written to this string builder. + * @param generateValid if the script should be guaranteed valid or if it is allowed to contain bugs. + */ + public ScriptGenerator(Random random, StringBuilder sb, boolean generateValid) { + this.random = random; + this.sb = sb; + this.generateValid = generateValid; + } + + /** + * Random number with log distribution + * @param max The maximum value + * @return A random number in the range [0, max) with higher numbers being more uncommon. + */ + private int randLog(int max) { + max--; + int n = 1<=0; i--) { + if ((num & (1<2) return false; + maybeInvalid = true; + return true; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return The index of the variable in variables or -1 + */ + private int lookup(String name) { + for (int i=variables.size()-1; i>=0; i--) { + if (variables.get(i).name.equals(name)) return i; + } + return -1; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return THe variable of null if not found + */ + private Variable lookupVar(String name) { + int idx = lookup(name); + return idx == -1 ? null : variables.get(idx); + } + + /** + * Add a variable to the current scope + * @param name The name of the variable + * @param type The declared type of the variable + */ + private void addVariable(String name, Type type) { + var old = lookupVar(name); + variables.add(new Variable(name, type, old)); + if (old != null) old.shadowed = true; + } + + /** + * Pop variables to level to + * @param to Level until which variables should be popped + */ + private void popVariables(int to) { + while (variables.size() > to) { + var v = variables.removeLast(); + if (v.shadowing != null) v.shadowing.shadowed = false; + } + } + + /** + * Get a random visible variable patching pred + * @param pred The predicate the variable needs to match + * @return A random visible variable matching pred or null if no variable mached pred. + */ + private Variable findVisibleVariablesMatching(Predicate pred) { + int num=0; + for (var v:variables) { + if (!v.shadowed && pred.test(v)) num++; + } + if (num == 0) return null; + num = random.nextInt(num); + for (var v:variables) { + if (!v.shadowed && pred.test(v) && num-- == 0) return v; + } + throw new AssertionError(); + } + + /** + * Get a structure by name + * @param name Name of the structure + * @return The structure of null + */ + private TypeStruct getStruct(String name) { + for (var s:structs) { + if (s.name.equals(name)) return s; + } + return null; + } + + /** + * Generate a variable name. This might be new random name or a name from an outer scope. + * If the program is allowed to be invalid this can return keywords and already used variable names in the current scope. + * @return The variable name. + */ + private String getVarName() { + if (currScopeStart > 0 && random.nextInt(10) > 7) { + // Use a variable outside the current scope. + var v = variables.get(random.nextInt(currScopeStart)); + if (lookup(v.name) < currScopeStart) return v.name; + if (!generateValid) { + maybeInvalid = true; + return v.name; + } + } + // Generate a new random variable name + StringBuilder sb = getRandomName(); + var v = sb.toString(); + if (!generateValid && (KEYWORDS.contains(v) || lookup(v) >= currScopeStart)) { + maybeInvalid = true; + return v; + } + 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; + addVariable(ScopeNode.ARG0, TYPE_INT); + currScopeStart = variables.size(); + if ((genStatements() & FLAG_STOP) == 0) { + if (generateValid || random.nextInt(10)<7) { + genReturn(); + } else { + this.maybeInvalid = true; + } + } + generateValid = oldGenerateValid; + return !this.maybeInvalid; + } + + /** + * Generate a list of statements. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the last statement generated + */ + public int genStatements() { + 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 -> 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; + var ret = genBlock(); + indentation += INDENTATION; + depth--; + yield ret; + } + case 6 -> genIf(); + case 7 -> genCountedLoop(); + case 8 -> genWhile(); + case 9 -> genAssignment(); + case 10 -> genNullCheck(); + default -> genExit(); + }; + depth++; + } + indentation -= INDENTATION; + allowStructs = oldAllowStructs; + return res; + } + + /** + * Generate an exit. This can be return or break and continue when in a loop. + * @return FLAG_STOP + */ + public int genExit() { + if (loopDepth == 0 && generateValid) return genReturn(); + return switch (random.nextInt(7)) { + case 0,1,2 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("continue;"); + yield FLAG_STOP; + } + case 3,4,5 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("break;"); + yield FLAG_STOP; + } + default -> genReturn(); + }; + } + + /** + * Generate a block statement. After a certain depth only empty blocks are generated to ensure termination of the generator. + * @return flag FLAG_STOP for the generated statement + */ + public int genBlock() { + if (depth == 0) { + sb.append("{}"); + return 0; + } + var oldAllowStructs = allowStructs; + var oldCSS = currScopeStart; + allowStructs = false; + currScopeStart = variables.size(); + depth--; + sb.append("{\n"); + indentation += INDENTATION; + var stop = genStatements(); + indentation -= INDENTATION; + printIndentation().append("}"); + popVariables(currScopeStart); + depth++; + currScopeStart = oldCSS; + allowStructs = oldAllowStructs; + return stop & ~FLAG_IF_WITHOUT_ELSE; + } + + /** + * Generate an if statement with an optional else block. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement + */ + public int genIf() { + sb.append("if("); + genExpression(TYPE_INT); + sb.append(") "); + var stop = genStatementBlock(); + if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { + sb.append("\n"); + printIndentation().append("else "); + stop &= genStatementBlock(); + } else { + stop = FLAG_IF_WITHOUT_ELSE; + } + 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(TYPE_INT); + sb.append(") "); + loopDepth++; + genStatementBlock(); + loopDepth--; + 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() { + 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(";"); + addVariable(name, type); + return 0; + } + + /** + * Generate an assignment statement. + * @return 0 + */ + public int genAssignment() { + 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(declared); + sb.append(";"); + return 0; + } + + /** + * Generate a return statement. + * @return FLAG_STOP + */ + public int genReturn() { + var type = getType(); + sb.append("return "); + genExpression(type); + sb.append(";"); + return FLAG_STOP; + } + + /** + * Generate a binary expression. + * This method does not care about operator precedence. + */ + 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(TYPE_INT); + sb.append(BINARY_OP[random.nextInt(BINARY_OP.length)]); + } + genUnary(TYPE_INT); + } + + /** + * Generate a unary expression. + */ + 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)]); + } + 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(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 constant. + */ + 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(Type type) { + if (generateInvalid()) { + sb.append(getRandomName()); + return; + } + 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); + } + +} diff --git a/chapter10b/Makefile b/chapter10b/Makefile new file mode 100644 index 00000000..5ed21084 --- /dev/null +++ b/chapter10b/Makefile @@ -0,0 +1,7 @@ +# Standalone compiler snapshot; shared rules also work in linearized checkouts. +include $(firstword $(wildcard ../build-support/early-chapter.mk build-support/early-chapter.mk)) +test_cp += com.seaofnodes.simple.FuzzerWrap + +.PHONY: fuzzer +fuzzer: $(test_classes) + java -ea -cp "$(CLZDIR)/main$(SEP)$(CLZDIR)/test$(SEP)$(jars)" org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap diff --git a/chapter10b/README.md b/chapter10b/README.md new file mode 100644 index 00000000..d90a9da7 --- /dev/null +++ b/chapter10b/README.md @@ -0,0 +1,175 @@ +# Chapter 10b: Splitting Memory + +[Previous: Chapter 10a](../chapter10a/README.md) | +[Next: Chapter 11](../chapter11/README.md) + +Chapter 10a made memory effects explicit with one SSA memory value. This +chapter keeps exactly that parser model and recovers independent memory chains +through graph rewrites. There is no new language syntax; the +[grammar](docs/10-grammar.md), pointer types, and null checks are unchanged. + +## Why split memory? + +```java +struct Point { int x; int y; } +Point p = new Point; +p.x = 42; +while (arg > 0) { + p.y = p.y + arg; + arg = arg - 1; +} +return p.x; +``` + +The loop changes `y`, so it cannot affect `x`. With one memory chain the load +of `x` follows the loop's stores. Splitting the fields lets ordinary +load-after-store forwarding turn the return value into 42. + +![Independent field dependencies](docs/example1.svg) + +These are schematic dependency graphs, with arrows from definitions to uses. +Control, pointer, and value edges are omitted when they do not explain memory. + +## Equivalence-class aliasing + +Each struct field has an integer alias identity. Two different fields receive +different identities, even if they have the same spelling in different structs. +Two instances of the same struct share their field aliases. Thus different +classes never alias; accesses in the same class **may alias**. Same-class does +not mean same-address: forwarding still checks pointer identity. + +The parser assigns field identities at struct declarations, beginning at 2. +These identities are independent of Start's output indices. The default slot +of a memory aggregate is 1; precise slots are greater than 1. + +We do not allocate a Start projection, scope binding, or loop Phi for every +declared field. Start supplies one memory value, Scope holds one `$mem` binding, +and Return consumes one complete memory value. The optimizer introduces slices +only where graph operations require them. + +## Three memory nodes + +| Node | Meaning | +|---|---| +| `MemMerge` | A complete memory state: a default plus precise alias overrides | +| `MemPhi` | One alias selected according to the incoming control path | +| `BulkMemPhi` | All aliases except an explicit exclusion set, selected by control | + +`MemMerge` combines independent pieces of memory at one program point. It is +not a control-flow merge. For example, after storing to alias `x`: + +```text +prior = current memory +store = Store_x(prior, pointer, value) +current memory = MemMerge(default=prior, x=store) +``` + +The Store selects its precise input from any incoming MemMerge. Loads and +precise memory Phis do the same. A missing precise entry falls back to the +default, provided that default still covers the requested alias. + +The explicit entries override the default: the semantic slices are disjoint, +and their union covers all memory. Flattening stacked aggregates preserves +entries that the outer aggregate has not replaced. This allows an `x` store +and a later `y` store to remain visible together at Return. + +![An aggregate preserves both updates](docs/example2a.svg) + +## Splitting a memory Phi + +Scope initially creates a BulkMemPhi wherever the ordinary SSA algorithm needs +to merge memory. Its empty exclusion set means that it covers every alias. +After the Region is complete, the Phi inspects its memory inputs and users. +When they require a precise alias, it splits out that alias: + +```text +BulkPhi(R, left, right) + becomes +MemMerge( + default = BulkPhi_except{x}(R, left, right), + x = MemPhi_x(R, slice(left,x), slice(right,x))) +``` + +The new precise Phi and the remaining bulk Phi share a Region and matching +control-path indices. A loop uses the same transformation, with the backedge +among those inputs. Each further split preserves all previously extracted +slices; replacing a bulk Phi must never drop their parallel precise Phis. + +![Precise and bulk Phis share a control merge](docs/example2b.svg) + +A bulk Phi can bypass an incoming aggregate only when all of that aggregate's +explicit slices are already excluded from the Phi. A precise consumer can +select one entry. A whole-memory consumer cannot just select the default: +that would lose the explicit updates. + +## Types and the worklist + +The memory type lattice adds a flat element `MEM#N` for each precise alias. +Meeting different aliases gives `TypeMem.BOT`. It does not track field values, +private objects, or escape information. Bulk exclusion sets belong to the +graph nodes, not to an expanded type lattice. + +![The type domains, including precise memory aliases](docs/lattice.svg) + +Scalar Phis remain ordinary PhiNodes. The scope's Phi factory chooses a +BulkMemPhi for whole memory; splitting constructs MemPhis explicitly. Generic +arithmetic factoring through Phis must not factor a memory aggregate. + +Memory splitting can increase node count. Progress comes from extracting an +alias from a bulk Phi and simplifying its precise chain, rather than from +requiring every rewrite to shrink the graph. Nodes must have conservative +initial types before introducing cyclic inputs. New bulk and precise Phis are +queued, so a recursive peephole cannot start another split halfway through the +first one. + +This also extends the worklist lesson from Chapter 9. A bulk Phi inspects its +users, so changes to a user's partition must wake the producer. Selecting a +slice from an aggregate can expose a new bulk-Phi user; that selected definition +must be queued too. The optimizer's worklist completeness assertion remains +enabled and checks these dependencies. + +## Evaluation and ordering + +MemMerge is a dependency node; it emits no heap operation. Its evaluator result +is a memory token, while Store mutates the object's fields. The scheduler must +distinguish aggregation from overwriting memory. A MemMerge is not a clobber, +and stores in different alias classes do not impose load/store anti-dependencies. + +![Ordering applies within the affected alias](docs/example2c.svg) + +Tests cover independent-field folding, parallel memory Phis, multiple field +updates surviving at Return, early returns, nested loops with break/continue, +and reads before subsequent stores through potentially aliased pointers. + +## What remains for later chapters? + +Chapter 10b knows every access's alias while parsing. What is lazy here is the +construction of memory partitions. Chapter 25 also allows an existing access's +alias to become known later, using the same representation. Its private +constructor memory, escape tracking, incomplete types, and module alias +remapping are separate extensions and are not needed here. + +Chapters 11-24 currently retain their older parser-managed memory chains. +Forwarding this representation through those snapshots is tracked in the +[backport queue](../docs/chapter-backports.md). Chapter 11's scheduling lesson +therefore still shows the older graph construction, with the same language. + +## Build and test + +With JDK 21 or newer, from this directory: + +```sh +make lib +make tests +make release +make view +``` + +Make builds this complete snapshot without Maven. `make release` writes +`build/release/simple.jar`; `make view` launches the shared graph viewer. +Both the root Maven reactor and the supplied IDEA module descriptor include +the chapter. From the repository root, test both halves with: + +```sh +make tests CHAPTERS="chapter10a chapter10b" +``` diff --git a/chapter10b/chapter10b.iml b/chapter10b/chapter10b.iml new file mode 100644 index 00000000..9d1eeeb2 --- /dev/null +++ b/chapter10b/chapter10b.iml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chapter10b/docs/10-grammar.md b/chapter10b/docs/10-grammar.md new file mode 100644 index 00000000..9a80d6ea --- /dev/null +++ b/chapter10b/docs/10-grammar.md @@ -0,0 +1,125 @@ +# Grammar for Chapter 10b + +```antlrv4 +grammar SimpleLanguage; + +program + : statement+ EOF + ; + +statement + : returnStatement + | structDeclaration + | declStatement + | blockStatment + | expressionStatement + | ifStatement + | whileStatement + | breakStatement + | continueStatment + ; + +field + : 'int' IDENTIFIER ';' + ; + +fields + : field+ + ; + +structDeclaration + : 'struct' IDENTIFIER '{' fields '}' + ; + +whileStatement + : 'while' '(' expression ')' statement + ; + +breakStatement + : 'break' ';' + ; + +continueStatement + : 'continue' ';' + ; + +ifStatement + : 'if' '(' expression ')' statement ('else' statement)? + ; + + +expressionStatement + : IDENTIFIER '=' expression ';' + | fieldExpression '=' expression ';' + ; + +blockStatement + : '{' statement+ '}' + ; + +structName + : IDENTIFIER + ; + +declStatement + : 'int' IDENTIFIER '=' expression ';' + | structName ('?')? IDENTIFIER '=' expression ';' + ; + +returnStatement + : 'return' expression ';' + ; + +expression + : comparisonExpression + ; + +comparisonExpression + : additiveExpression (('==' | '!='| '>'| '<'| '>='| '<=') additiveExpression)* + ; + +additiveExpression + : multiplicativeExpression (('+' | '-') multiplicativeExpression)* + ; + +multiplicativeExpression + : unaryExpression (('*' | '/') unaryExpression)* + ; + +unaryExpression + : ('-') unaryExpression + | '!' unaryExpression + | primaryExpression + ; + +newExpression + : 'new' IDENTIFIER + ; + +fieldExpression + : primaryExpresson '.' IDENTIFIER + ; + +primaryExpression + : IDENTIFIER + | INTEGER_LITERAL + | 'true' + | 'false' + | 'null' + | newExpression + | '(' expression ')' + | fieldExpression + ; + +INTEGER_LITERAL + : [1-9][0-9]* + | [0] + ; + +IDENTIFIER + : NON_DIGIT (NON_DIGIT | DIGIT)* + ; + +NON_DIGIT: [a-zA-Z_]; +DEC_DIGIT: [0-9]; +``` diff --git a/chapter10b/docs/Makefile b/chapter10b/docs/Makefile new file mode 100644 index 00000000..055882b2 --- /dev/null +++ b/chapter10b/docs/Makefile @@ -0,0 +1,14 @@ +GV := $(wildcard *.gv) +SVG := $(patsubst %.gv,%.svg,$(GV)) +.DELETE_ON_ERROR: + +.PHONY: all +all: $(SVG) + +%.svg: %.gv + dot -Tsvg $< > $@ + @sed -i 's/\r$$//' $@ + +.PHONY: clean +clean: + @rm -f $(SVG) diff --git a/chapter10b/docs/example1.gv b/chapter10b/docs/example1.gv new file mode 100644 index 00000000..d1b2d456 --- /dev/null +++ b/chapter10b/docs/example1.gv @@ -0,0 +1,11 @@ +digraph Memory { + rankdir=LR; + node [shape=box]; + M [label="Initial memory"]; + X [label="Store x = 42"]; + Y [label="Loop: Store y"]; + L [label="Load x -> 42"]; + M -> X -> L [color=blue]; M -> Y [color=green]; + X -> MemMerge [color=blue]; Y -> MemMerge [color=green]; + MemMerge -> Return; L -> Return; +} diff --git a/chapter10b/docs/example1.svg b/chapter10b/docs/example1.svg new file mode 100644 index 00000000..bcd2a221 --- /dev/null +++ b/chapter10b/docs/example1.svg @@ -0,0 +1,91 @@ + + + + + + +Memory + + + +M + +Initial memory + + + +X + +Store x = 42 + + + +M->X + + + + + +Y + +Loop: Store y + + + +M->Y + + + + + +L + +Load x -> 42 + + + +X->L + + + + + +MemMerge + +MemMerge + + + +X->MemMerge + + + + + +Y->MemMerge + + + + + +Return + +Return + + + +L->Return + + + + + +MemMerge->Return + + + + + diff --git a/chapter10b/docs/example2a.gv b/chapter10b/docs/example2a.gv new file mode 100644 index 00000000..50f7ec80 --- /dev/null +++ b/chapter10b/docs/example2a.gv @@ -0,0 +1,10 @@ +digraph Memory { + rankdir=LR; + node [shape=box]; + M [label="Default memory"]; + X [label="Store x"]; + Y [label="Store y"]; + M -> X [color=blue]; M -> Y [color=green]; + M -> MemMerge [label="default"]; X -> MemMerge [label="x",color=blue]; + Y -> MemMerge [label="y",color=green]; MemMerge -> Return; +} diff --git a/chapter10b/docs/example2a.svg b/chapter10b/docs/example2a.svg new file mode 100644 index 00000000..74cb48e0 --- /dev/null +++ b/chapter10b/docs/example2a.svg @@ -0,0 +1,82 @@ + + + + + + +Memory + + + +M + +Default memory + + + +X + +Store x + + + +M->X + + + + + +Y + +Store y + + + +M->Y + + + + + +MemMerge + +MemMerge + + + +M->MemMerge + + +default + + + +X->MemMerge + + +x + + + +Y->MemMerge + + +y + + + +Return + +Return + + + +MemMerge->Return + + + + + diff --git a/chapter10b/docs/example2b.gv b/chapter10b/docs/example2b.gv new file mode 100644 index 00000000..7ef10ec8 --- /dev/null +++ b/chapter10b/docs/example2b.gv @@ -0,0 +1,10 @@ +digraph Memory { + rankdir=LR; + node [shape=box]; + P [label="MemPhi(x)"]; + B [label="BulkMemPhi(All except x)"]; + Left -> P [label="slice x",color=blue]; Right -> P [label="slice x",color=blue]; + Left -> B; Right -> B; + Region -> P [style=dashed]; Region -> B [style=dashed]; + P -> MemMerge [label="x",color=blue]; B -> MemMerge [label="default"]; +} diff --git a/chapter10b/docs/example2b.svg b/chapter10b/docs/example2b.svg new file mode 100644 index 00000000..fae583e6 --- /dev/null +++ b/chapter10b/docs/example2b.svg @@ -0,0 +1,101 @@ + + + + + + +Memory + + + +P + +MemPhi(x) + + + +MemMerge + +MemMerge + + + +P->MemMerge + + +x + + + +B + +BulkMemPhi(All except x) + + + +B->MemMerge + + +default + + + +Left + +Left + + + +Left->P + + +slice x + + + +Left->B + + + + + +Right + +Right + + + +Right->P + + +slice x + + + +Right->B + + + + + +Region + +Region + + + +Region->P + + + + + +Region->B + + + + + diff --git a/chapter10b/docs/example2c.gv b/chapter10b/docs/example2c.gv new file mode 100644 index 00000000..fb827b3e --- /dev/null +++ b/chapter10b/docs/example2c.gv @@ -0,0 +1,10 @@ +digraph Memory { + rankdir=LR; + node [shape=box]; + M [label="Prior x memory"]; + L [label="Load p.x"]; + S [label="Store p.x"]; + M -> L [color=blue]; M -> S [color=blue]; + L -> S [style=dashed,label="execute load first"]; + Y [label="Store p.y: independent"]; +} diff --git a/chapter10b/docs/example2c.svg b/chapter10b/docs/example2c.svg new file mode 100644 index 00000000..d8e5cf96 --- /dev/null +++ b/chapter10b/docs/example2c.svg @@ -0,0 +1,56 @@ + + + + + + +Memory + + + +M + +Prior x memory + + + +L + +Load p.x + + + +M->L + + + + + +S + +Store p.x + + + +M->S + + + + + +L->S + + +execute load first + + + +Y + +Store p.y: independent + + + diff --git a/chapter10b/docs/lattice.gv b/chapter10b/docs/lattice.gv new file mode 100644 index 00000000..f87acfed --- /dev/null +++ b/chapter10b/docs/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="#2" , 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/chapter10b/docs/lattice.svg b/chapter10b/docs/lattice.svg new file mode 100644 index 00000000..4023b68c --- /dev/null +++ b/chapter10b/docs/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 + +#2 + + + +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/chapter10b/pom.xml b/chapter10b/pom.xml new file mode 100644 index 00000000..fb7c947f --- /dev/null +++ b/chapter10b/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + com.seaofnodes + simple + 1.0 + + chapter10b + jar + Chapter 10b + + ${project.basedir}/../graph + + diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/IRPrinter.java b/chapter10b/src/main/java/com/seaofnodes/simple/IRPrinter.java new file mode 100644 index 00000000..1ac32b1a --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/IRPrinter.java @@ -0,0 +1,188 @@ +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 { + + // Print a node on 1 line, columnar aligned, as: + // NNID NNAME DDEF DDEF [[ UUSE UUSE ]] TYPE + // 1234 sssss 1234 1234 1234 1234 1234 1234 tttttt + public static void _printLine( Node n, StringBuilder sb ) { + sb.append("%4d %-7.7s ".formatted(n._nid,n.label())); + if( n.isDead() ) { + sb.append("DEAD\n"); + return; + } + for( Node def : n._inputs ) + sb.append(def==null ? "____ " : "%4d ".formatted(def._nid)); + for( int i = n._inputs.size(); i<3; i++ ) + sb.append(" "); + sb.append(" [[ "); + for( Node use : n._outputs ) + sb.append(use==null ? "____ " : "%4d ".formatted(use._nid)); + int lim = 5 - Math.max(n._inputs.size(),3); + for( int i = n._outputs.size(); i 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 + ArrayList rpos = new ArrayList<>(); + BitSet visit = new BitSet(); + for( int i=bfs._lim; i< bfs._bfs.size(); i++ ) + postOrd( bfs._bfs.get(i), rpos, visit, bfs._bs); + // Reverse the post-order walk + StringBuilder sb = new StringBuilder(); + boolean gap=false; + for( int i=rpos.size()-1; i>=0; i-- ) { + Node n = rpos.get(i); + if( n.isCFG() || n.isMultiHead() ) { + if( !gap ) sb.append("\n"); // Blank before multihead + printLine( n, sb, llvmFormat ); // Print head + while( --i >= 0 ) { + Node t = rpos.get(i); + if( !t.isMultiTail() ) { i++; break; } + printLine( t, sb, llvmFormat ); + } + sb.append("\n"); // Blank after multitail + gap = true; + } else { + printLine( n, sb, llvmFormat ); + gap = false; + } + } + return sb.toString(); + } + + private static void postOrd(Node n, ArrayList rpos, BitSet visit, BitSet bfs) { + if( !bfs.get(n._nid) ) + return; // Not in the BFS visit + if( visit.get(n._nid) ) return; // Already post-order walked + visit.set(n._nid); + // First walk the CFG, then everything + if( n.isCFG() ) { + for( Node use : n._outputs ) + if( use!=null && use.isCFG() && use.nOuts()>=1 && !(use._outputs.get(0) instanceof LoopNode) ) + postOrd(use, rpos,visit,bfs); + for( Node use : n._outputs ) + if( use!=null && use.isCFG() ) + postOrd(use,rpos,visit,bfs); + } + for( Node use : n._outputs ) + if( use != null ) + postOrd(use, rpos,visit,bfs); + // Post-order + rpos.add(n); + } + + // Breadth-first search, broken out in a class to keep in more independent. + // Maintains a root-set of Nodes at the limit (or past by 1 if MultiHead). + public static class BFS { + // A breadth first search, plus MultiHeads for any MultiTails + public final ArrayList _bfs; + public final BitSet _bs; // Visited members by node id + public final int _depth; // Depth limit + public final int _lim; // From here to _bfs._len can be roots for a reverse search + public BFS( Node base, int d ) { + _depth = d; + _bfs = new ArrayList<>(); + _bs = new BitSet(); + + add(base); // Prime the pump + int idx=0, lim=1; // Limit is where depth counter changes + while( idx < _bfs.size() ) { // Ran out of nodes below depth + Node n = _bfs.get(idx++); + for( Node def : n._inputs ) + if( def!=null && !_bs.get(def._nid) ) + add(def); + if( idx==lim ) { // Depth counter changes at limit + if( --d < 0 ) + break; // Ran out of depth + lim = _bfs.size(); // New depth limit + } + } + // Toss things past the limit except multi-heads + while( idx < _bfs.size() ) { + Node n = _bfs.get(idx); + if( n.isMultiHead() ) idx++; + else del(idx); + } + // Root set is any node with no inputs in the visited set + lim = _bfs.size(); + for( int i=_bfs.size()-1; i>=0; i-- ) + if( !any_visited(_bfs.get(i)) ) + swap( i,--lim); + _lim = lim; + } + void swap( int x, int y ) { + if( x==y ) return; + Node tx = _bfs.get(x); + Node ty = _bfs.get(y); + _bfs.set(x,ty); + _bfs.set(y,tx); + } + void add(Node n) { + _bfs.add(n); + _bs.set(n._nid); + } + void del(int idx) { + Node n = Utils.del(_bfs, idx); + _bs.clear(n._nid); + } + boolean any_visited( Node n ) { + for( Node def : n._inputs ) + if( def!=null && _bs.get(def._nid) ) + return true; + return false; + } + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/IterPeeps.java b/chapter10b/src/main/java/com/seaofnodes/simple/IterPeeps.java new file mode 100644 index 00000000..7075d526 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/IterPeeps.java @@ -0,0 +1,208 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Random; + +/** + * The IterOptim runs after parsing. It iterates the peepholes to a fixed point + * so that no more peepholes apply. This should be linear because peepholes rarely + * (never?) increase code size. The graph should monotonically reduce in some + * dimension, which is usually size. It might also reduce in e.g. number of + * MulNodes or Load/Store nodes, swapping out more "expensive" Nodes for cheaper + * ones. + * + * The theoretical overall worklist is mindless just grabbing the next thing and + * doing it. If the graph changes, put the neighbors on the worklist. Lather, + * Rinse, Repeat until the worklist runs dry. + * + * The main issues we have to deal with: + * + *
    + *
  • Nodes have uses; replacing some set of Nodes with another requires more graph + * reworking. Not rocket science, but it can be fiddly. Its helpful to have a + * small set of graph munging utilities, and the strong invariant that the graph + * is stable and correct between peepholes. In our case `Node.subsume` does + * most of the munging, building on our prior stable Node utilities.
  • + * + *
  • Changing a Node also changes the graph "neighborhood". The neigbors need to + * be checked to see if THEY can also peephole, and so on. After any peephole + * or graph update we put a Nodes uses and defs on the worklist.
  • + * + *
  • Our strong invariant is that for all Nodes, either they are on the worklist + * OR no peephole applies. This invariant is easy to check, although expensive. + * Basically the normal "iterate peepholes to a fixed point" is linear, and this + * check is linear at each peephole step... so quadratic overall. Its a useful + * assert, but one we can disable once the overall algorithm is stable - and + * then turn it back on again when some new set of peepholes is misbehaving. + * The code for this is turned on in `IterOptim.iterate` as `assert + * progressOnList(stop);`
  • + *
+ */ +public abstract class IterPeeps { + + private static final WorkList WORK = new WorkList<>(); + + public static N add( N n ) { + return (N)WORK.push(n); + } + + public static void addAll( ArrayList ary ) { + WORK.addAll(ary); + } + + /** + * Iterate peepholes to a fixed point + */ + public static StopNode iterate(StopNode stop) { + assert progressOnList(stop); + int cnt=0; + + Node n; + while( (n=WORK.pop()) != null ) { + if( n.isDead() ) continue; + cnt++; // Useful for debugging, searching which peephole broke things + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + if( obs != null ) obs.before(n); + Node x = n.peepholeOpt(); + if( x != null ) { + if( x.isDead() ) { + if( obs != null ) obs.after(n, x, true); + continue; + } + // peepholeOpt can return brand-new nodes, needing an initial type set + if( x._type==null ) x.setType(x.compute()); + // Changes require neighbors onto the worklist + if( x != n || !(x instanceof ConstantNode) ) { + // All outputs of n (changing node) not x (prior existing node). + for( Node z : n._outputs ) WORK.push(z); + // Everybody gets a free "go again" in case they didn't get + // made in their final form. + WORK.push(x); + // If the result is not self, revisit all inputs (because + // there's a new user), and replace in the graph. + if( x != n ) { + for( Node z : n. _inputs ) WORK.push(z); + n.subsume(x); + } + } + // If there are distant neighbors, move to worklist + n.moveDepsToWorklist(); + assert progressOnList(stop); // Very expensive assert + } + if( n.isUnused() && !(n instanceof StopNode) ) + n.kill(); // Just plain dead + if( obs != null ) obs.after(n, x, true); + } + + return stop; + } + + // Visit ALL nodes and confirm the invariant: + // Either you are on the WORK worklist OR running `iter()` makes no progress. + + // This invariant ensures that no progress is missed, i.e., when the + // worklist is empty we have indeed done all that can be done. To help + // with debugging, the {@code assert} is broken out in a place where it is easy to + // stop if a change is found. + + // Also, the normal usage of `iter()` may attempt peepholes with distance + // neighbors and these should fail, but will then try to add dependencies + // {@link #Node.addDep} which is a side effect in an assert. The {@link + // #midAssert} is used to stop this side effect. + private static boolean MID_ASSERT; + public static boolean midAssert() { return MID_ASSERT; } + private static boolean progressOnList(Node stop) { + MID_ASSERT = true; + int old_cnt = Node.ITER_CNT, old_nop = Node.ITER_NOP_CNT; + Node changed = stop.walk( n -> { + if( WORK.on(n) ) return null; + Node m = n.peepholeOpt(); + if( m==null ) return null; + System.err.println("BREAK HERE FOR BUG"); + return m; + }); + Node.ITER_CNT = old_cnt; Node.ITER_NOP_CNT = old_nop; + MID_ASSERT = false; + return changed==null; + } + + public static void reset() { + WORK.clear(); + } + + /** + * Classic WorkList, with a fast add/remove, dup removal, random pull. + * The Node's nid is used to check membership in the worklist. + */ + public static class WorkList { + + private Node[] _es; + private int _len; + private final BitSet _on; // Bit set if Node._nid is on WorkList + private final Random _R; // For randomizing pull from the WorkList + private final long _seed; + + /* Useful stat - how many nodes are processed in the post parse iterative opt */ + private long _totalWork = 0; + + WorkList() { this(123); } + WorkList(long seed) { + _es = new Node[1]; + _len=0; + _on = new BitSet(); + _seed = seed; + _R = new Random(); + _R.setSeed(_seed); + } + + /** + * Pushes a Node on the WorkList, ensuring no duplicates + * If Node is null it will not be added. + */ + public E push( E x ) { + if( x==null ) return null; + int idx = x._nid; + if( !_on.get(idx) ) { + _on.set(idx); + if( _len==_es.length ) + _es = Arrays.copyOf(_es,_len<<1); + _es[_len++] = x; + _totalWork++; + } + return x; + } + + public void addAll( ArrayList ary ) { + for( E n : ary ) + push(n); + } + + /** + * True if Node is on the WorkList + */ + boolean on( E x ) { return _on.get(x._nid); } + + /** + * Removes a random Node from the WorkList; null if WorkList is empty + */ + E pop() { + if( _len == 0 ) return null; + int idx = _R.nextInt(_len); + E x = (E)_es[idx]; + _es[idx] = _es[--_len]; // Compress array + _on.clear(x._nid); + return x; + } + + public void clear() { + _len = 0; + _on.clear(); + _R.setSeed(_seed); + _totalWork = 0; + } + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/Parser.java b/chapter10b/src/main/java/com/seaofnodes/simple/Parser.java new file mode 100644 index 00000000..3b5f2721 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/Parser.java @@ -0,0 +1,843 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.graph.GraphObserver; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +import java.util.*; + +/** + * The Parser converts a Simple source program to the Sea of Nodes intermediate + * representation directly in one pass. There is no intermediate Abstract + * Syntax Tree structure. + *

+ * This is a simple recursive descent parser. All lexical analysis is done here as well. + */ +public class Parser { + + // Current compilation context; the observer belongs to the parser. + public static Parser PARSER; + public GraphObserver _obs; + public int pos() { return _lexer._position; } + + + /** + * A Global Static, unique to each compilation. This is a public, so we + * can make constants everywhere without having to thread the StartNode + * through the entire parser and optimizer. + *

+ * To make the compiler multithreaded, this field will have to move into a TLS. + */ + public static StartNode START; + + public StopNode STOP; + + // Field identities are independent of the memory graph and Start's tuple. + private final HashMap _aliases = new HashMap<>(); + private int _alias = 2; + + // The Lexer. Thin wrapper over a byte[] buffer with a cursor. + private final Lexer _lexer; + + /** + * Current ScopeNode - ScopeNodes change as we parse code, but at any point of time + * there is one current ScopeNode. The reason the current ScopeNode can change is to do with how + * we handle branching. See {@link #parseIf()}. + *

+ * Each ScopeNode contains a stack of lexical scopes, each scope is a symbol table that binds + * variable names to Nodes. The top of this stack represents current scope. + *

+ * We keep a list of all ScopeNodes so that we can show them in graphs. + * @see #parseIf() + * @see #_xScopes + */ + public ScopeNode _scope; + + /** + * List of keywords disallowed as identifiers + */ + 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"); + }}; + + + /** + * We clone ScopeNodes when control flows branch; it is useful to have + * a list of all active ScopeNodes for purposes of visualization of the SoN graph + */ + public final Stack _xScopes = new Stack<>(); + + ScopeNode _continueScope; + ScopeNode _breakScope; + + // Mapping from a Struct name to a Struct. + public static Map OBJS = new HashMap<>(); + + public Parser(String source, TypeInteger arg) { + PARSER = this; + Node.reset(); + IterPeeps.reset(); + OBJS.clear(); + _lexer = new Lexer(source); + _scope = new ScopeNode(); + _continueScope = _breakScope = null; + START = new StartNode(new Type[]{ Type.CONTROL, TypeMem.BOT, arg }); + STOP = new StopNode(source); + } + + public Parser(String source) { + this(source, TypeInteger.BOT); + } + + @Override + public String toString() { return _lexer.toString(); } + + String src() { return new String( _lexer._input ); } + + // Debugging utility to find a Node by index + public static Node find(int nid) { return START.find(nid); } + + private Node ctrl() { return _scope.ctrl(); } + + private Node ctrl(Node n) { return _scope.ctrl(n); } + + public StopNode parse() { + _xScopes.push(_scope); + // Enter a new scope for the initial control and arguments + _scope.push(); + _scope.define(ScopeNode.CTRL, Type.CONTROL , new ProjNode(START, 0, ScopeNode.CTRL).peephole()); + _scope.define(ScopeNode.ARG0, TypeInteger.BOT, new ProjNode(START, 2, ScopeNode.ARG0).peephole()); + _scope.define("$mem", TypeMem.BOT, new ProjNode(START, 1, "$mem").peephole()); + parseBlock(); + _scope.pop(); + _xScopes.pop(); + if (!_lexer.isEOF()) throw error("Syntax error, unexpected " + _lexer.getAnyNextToken()); + STOP.peephole(); + return STOP; + } + + /** + * Parses a block + * + *

+     *     '{' statements '}'
+     * 
+ * Does not parse the opening or closing '{}' + * @return a {@link Node} or {@code null} + */ + private Node parseBlock() { + // Enter a new scope + _scope.push(); + while (!peek('}') && !_lexer.isEOF()) + parseStatement(); + // Exit scope + _scope.pop(); + return null; + } + + /** + * Parses a statement + * + *
+     *     returnStatement | declStatement | blockStatement | ifStatement | expressionStatement
+     * 
+ * @return a {@link Node} or {@code null} + */ + private Node parseStatement() { + 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(";") ) 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); + _aliases.put(typeName,_alias); + _alias += ts._fields.length; + OBJS.put(typeName, ts); // Insert the struct name in the collection of all struct names + return parseStatement(); + } + + /** + * Parses a while statement + * + *
+     *     while ( expression ) statement
+     * 
+ * @return a {@link Node}, never {@code null} + */ + private Node parseWhile() { + + var savedContinueScope = _continueScope; + var savedBreakScope = _breakScope; + + require("("); + + // Loop region has two control inputs, the first is the entry + // point, and second is back edge that is set after loop is parsed + // (see end_loop() call below). Note that the absence of back edge is + // used as an indicator to switch off peepholes of the region and + // associated phis; see {@code inProgress()}. + + ctrl(new LoopNode(ctrl()).peephole()); // Note we set back edge to null here + + // At loop head, we clone the current Scope (this includes all + // names in every nesting level within the Scope). + // We create phis eagerly for all the names we find, see dup(). + + // Save the current scope as the loop head + ScopeNode head = _scope.keep(); + // Clone the head Scope to create a new Scope for the body. + // Create phis eagerly as part of cloning + _xScopes.push(_scope = _scope.dup(true)); // The true argument triggers creating phis + + // Parse predicate + var pred = require(parseExpression(), ")"); + // IfNode takes current control and predicate + Node ifNode = new IfNode(ctrl(), pred).peephole(); + // Setup projection nodes + Node ifT = new ProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + Node ifF = new ProjNode(ifNode.unkeep(), 1, "False").peephole(); + + // Clone the body Scope to create the break/exit Scope which accounts for any + // side effects in the predicate. The break/exit Scope will be the final + // scope after the loop, and its control input is the False branch of + // the loop predicate. Note that body Scope is still our current scope. + ctrl(ifF); + _xScopes.push(_breakScope = _scope.dup()); + + // No continues yet + _continueScope = null; + + // Parse the true side, which corresponds to loop body + // Our current scope is the body Scope + ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection + parseStatement(); // Parse loop body + + // Merge the loop bottom into other continue statements + if (_continueScope != null) { + _continueScope = jumpTo(_continueScope); + _scope.kill(); + _scope = _continueScope; + } + + // The true branch loops back, so whatever is current _scope.ctrl gets + // added to head loop as input. endLoop() updates the head scope, and + // goes through all the phis that were created earlier. For each phi, + // it sets the second input to the corresponding input from the back + // edge. If the phi is redundant, it is replaced by its sole input. + var exit = _breakScope; + head.endLoop(_scope, exit); + head.unkeep().kill(); + + _xScopes.pop(); // Cleanup + _xScopes.pop(); // Cleanup + + _continueScope = savedContinueScope; + _breakScope = savedBreakScope; + + // At exit the false control is the current control, and + // the scope is the exit scope after the exit test. + return _scope = exit; + } + + private ScopeNode jumpTo(ScopeNode toScope) { + ScopeNode cur = _scope.dup(); + ctrl(new ConstantNode(Type.XCONTROL).peephole()); // Kill current scope + // Prune nested lexical scopes that have depth > than the loop head + // We use _breakScope as a proxy for the loop head scope to obtain the depth + while( cur._scopes.size() > _breakScope._scopes.size() ) + 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" + if (toScope == null) + return cur; + // toScope is either the break scope, or a scope that was created here + assert toScope._scopes.size() <= _breakScope._scopes.size(); + toScope.ctrl(toScope.mergeScopes(cur)); + return toScope; + } + + private void checkLoopActive() { if (_breakScope == null) throw Parser.error("No active loop for a break or continue"); } + + private Node parseBreak () { checkLoopActive(); return ( _breakScope = require(jumpTo( _breakScope ),";")); } + private Node parseContinue() { checkLoopActive(); return (_continueScope = require(jumpTo( _continueScope ),";")); } + + /** + * Parses a statement + * + *
+     *     if ( expression ) statement [else statement]
+     * 
+ * @return a {@link Node}, never {@code null} + */ + private Node parseIf() { + require("("); + // Parse predicate + var pred = require(parseExpression(), ")").keep(); + // IfNode takes current control and predicate + Node ifNode = new IfNode(ctrl(), pred).peephole(); + // Setup projection nodes + Node ifT = new ProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + Node ifF = new ProjNode(ifNode.unkeep(), 1, "False").peephole().keep(); + // In if true branch, the ifT proj node becomes the ctrl + // But first clone the scope and set it as current + int ndefs = _scope.nIns(); + ScopeNode fScope = _scope.dup(); // Duplicate current scope + fScope.ctrl(ifF); // The pending arm already starts at False + _xScopes.push(fScope); // For graph visualization we need all scopes + + // 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; + + // Parse the false side + _scope = fScope; // Restore scope, then parse else block if any + ifF.unkeep(); // fScope already owns the false control + if (matchx("else")) { + _scope.upcast(ifF,pred,true); // Up-cast predicate + parseStatement(); + fScope = _scope; + } + pred.unkeep(); + + if( tScope.nIns() != ndefs || fScope.nIns() != ndefs ) + throw error("Cannot define a new name on one arm of an if"); + + // Merge results + _scope = tScope; + _xScopes.pop(); // Discard pushed from graph display + + return ctrl(tScope.mergeScopes(fScope)); + } + + + /** + * Parses a return statement; "return" already parsed. + * The $ctrl edge is killed. + * + *
+     *     'return' expr ;
+     * 
+ * @return an expression {@link Node}, never {@code null} + */ + private Node parseReturn() { + var expr = require(parseExpression(), ";"); + Node ret = STOP.addReturn(new ReturnNode(ctrl(), expr, _scope).peephole()); + ctrl(new ConstantNode(Type.XCONTROL).peephole()); // Kill control + return ret; + } + + + + /** + * Parses an expression statement or a declaration statement where type is a struct + * + *
+     *      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() { + 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 + * + *
+     *     type name = expression ';'
+     * 
+ * @return an expression {@link Node} + */ + private Node parseDecl(Type t) { + var name = requireId(); + 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: + * + *
+     *     expr : compareExpr
+     * 
+ * @return an expression {@link Node}, never {@code null} + */ + private Node parseExpression() { return parseComparison(); } + + /** + * Parse an expression of the form: + * + *
+     *     expr : additiveExpr op additiveExpr
+     * 
+ * @return an comparator expression {@link Node}, never {@code null} + */ + private Node parseComparison() { + var lhs = parseAddition(); + while( true ) { + int idx=0; boolean negate=false; + // Test for any local nodes made, and "keep" lhs during peepholes + if( false ) ; + else if( match("==") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); } + else if( match("!=") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); negate=true; } + else if( match("<=") ) { idx=2; lhs = new BoolNode.LE(lhs, null); } + else if( match("<" ) ) { idx=2; lhs = new BoolNode.LT(lhs, null); } + else if( match(">=") ) { idx=1; lhs = new BoolNode.LE(null, lhs); } + else if( match(">" ) ) { idx=1; lhs = new BoolNode.LT(null, lhs); } + else break; + // Peepholes can fire, but lhs is already "hooked", kept alive + lhs.setDef(idx,parseAddition()); + lhs = lhs.peephole(); + if( negate ) // Extra negate for != + lhs = new NotNode(lhs).peephole(); + } + return lhs; + } + + /** + * Parse an additive expression + * + *
+     *     additiveExpr : multiplicativeExpr (('+' | '-') multiplicativeExpr)*
+     * 
+ * @return an add expression {@link Node}, never {@code null} + */ + private Node parseAddition() { + Node lhs = parseMultiplication(); + while( true ) { + if( false ) ; + else if( match("+") ) lhs = new AddNode(lhs,null); + else if( match("-") ) lhs = new SubNode(lhs,null); + else break; + lhs.setDef(2,parseMultiplication()); + lhs = lhs.peephole(); + } + return lhs; + } + + /** + * Parse an multiplicativeExpr expression + * + *
+     *     multiplicativeExpr : unaryExpr (('*' | '/') unaryExpr)*
+     * 
+ * @return a multiply expression {@link Node}, never {@code null} + */ + private Node parseMultiplication() { + var lhs = parseUnary(); + while( true ) { + if( false ) ; + else if( match("*") ) lhs = new MulNode(lhs,null); + else if( match("/") ) lhs = new DivNode(lhs,null); + else break; + lhs.setDef(2,parseUnary()); + lhs = lhs.peephole(); + } + return lhs; + } + + /** + * Parse a unary minus expression. + * + *
+     *     unaryExpr : ('-') | '!') unaryExpr | postfixExpr | primaryExpr
+     * 
+ * @return a unary expression {@link Node}, never {@code null} + */ + private Node parseUnary() { + if (match("-")) return new MinusNode(parseUnary()).peephole(); + if (match("!")) return new NotNode(parseUnary()).peephole(); + return parsePostfix(parsePrimary()); + } + + /** + * Parse a primary expression: + * + *
+     *     primaryExpr : integerLiteral | Identifier | true | false | null | new Identifier | '(' expression ')'
+     * 
+ * @return a primary {@link Node}, never {@code null} + */ + private Node parsePrimary() { + if( _lexer.isNumber() ) return parseIntegerLiteral(); + 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); + if( n!=null ) return n; + 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 = _aliases.get(obj._name); + for( Field field : obj._fields ) { + store(field._fname,alias,n,initValue); + alias++; + } + return n.unkeep(); + } + + // Memory is one hidden SSA variable, including across branches and loops. + private Node mem() { return _scope.lookup("$mem"); } + private Node mem(Node n) { return _scope.update("$mem",n); } + + private void store(String name, int alias, Node ptr, Node val) { + Node prior = mem().keep(); + Node st = new StoreNode(name,alias,prior,ptr,val).peephole(); + mem(new MemMergeNode(prior,alias,st).peephole()); + prior.unkeep(); + } + + /** + * 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 = _aliases.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(); + store(name,alias,expr,val); + 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, mem(), expr).peephole()); + } + + /** + * Parse integer literal + * + *
+     *     integerLiteral: [1-9][0-9]* | [0]
+     * 
+ */ + private ConstantNode parseIntegerLiteral() { + return (ConstantNode) new ConstantNode(_lexer.parseNumber()).peephole(); + } + + ////////////////////////////////// + // Utilities for lexical analysis + + // Return true and skip if "syntax" is next in the stream. + private boolean match (String syntax) { return _lexer.match (syntax); } + // Match must be "exact", not be followed by more id letters + 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() { + String id = _lexer.matchId(); + if (id != null && !KEYWORDS.contains(id) ) return id; + throw error("Expected an identifier, found '"+id+"'"); + } + + + + // Require an exact match + 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); + } + + RuntimeException errorSyntax(String syntax) { + return error("Syntax error, expected " + syntax + ": " + _lexer.getAnyNextToken()); + } + + public static RuntimeException error( String errorMessage ) { + return new RuntimeException(errorMessage); + } + + //////////////////////////////////// + // Lexer components + + private static class Lexer { + + // Input buffer; an array of text bytes read from a file or a string + private final byte[] _input; + // Tracks current position in input buffer + private int _position = 0; + + /** + * Record the source text for lexing + */ + public Lexer(String source) { + this(source.getBytes()); + } + + /** + * Direct from disk file source + */ + public Lexer(byte[] buf) { + _input = buf; + } + + // Very handy in the debugger, shows the unparsed program + @Override + public String toString() { + return new String(_input, _position, _input.length - _position); + } + + // True if at EOF + private boolean isEOF() { + return _position >= _input.length; + } + + // Peek next character, or report EOF + private char peek() { + return isEOF() ? Character.MAX_VALUE // Special value that causes parsing to terminate + : (char) _input[_position]; + } + + private char nextChar() { + char ch = peek(); + _position++; + return ch; + } + + // True if a white space + private boolean isWhiteSpace() { + return peek() <= ' '; // Includes all the use space, tab, newline, CR + } + + /** + * Return the next non-white-space character + */ + private void skipWhiteSpace() { + while (isWhiteSpace()) _position++; + } + + + // Return true, if we find "syntax" after skipping white space; also + // then advance the cursor past syntax. + // Return false otherwise, and do not advance the cursor. + boolean match(String syntax) { + skipWhiteSpace(); + int len = syntax.length(); + if (_position + len > _input.length) return false; + for (int i = 0; i < len; i++) + if ((char) _input[_position + i] != syntax.charAt(i)) + return false; + _position += len; + 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; + _position -= syntax.length(); + return false; + } + + private boolean peek(char ch) { + skipWhiteSpace(); + return peek()==ch; + } + + boolean peekIsId() { + skipWhiteSpace(); + return isIdStart(peek()); + } + + // Return an identifier or null + String matchId() { + return peekIsId() ? parseId() : null; + } + + // Used for errors + String getAnyNextToken() { + if (isEOF()) return ""; + if (isIdStart(peek())) return parseId(); + if (isNumber(peek())) return parseNumberString(); + if (isPunctuation(peek())) return parsePunctuation(); + return String.valueOf(peek()); + } + + boolean isNumber() {return isNumber(peek());} + boolean isNumber(char ch) {return Character.isDigit(ch);} + + private Type parseNumber() { + String snum = parseNumberString(); + if (snum.length() > 1 && snum.charAt(0) == '0') + throw error("Syntax error: integer values cannot start with '0'"); + return TypeInteger.constant(Long.parseLong(snum)); + } + private String parseNumberString() { + int start = _position; + while (isNumber(nextChar())) ; + return new String(_input, start, --_position - start); + } + + // First letter of an identifier + private boolean isIdStart(char ch) { + return Character.isAlphabetic(ch) || ch == '_'; + } + + // All characters of an identifier, e.g. "_x123" + private boolean isIdLetter(char ch) { + return Character.isLetterOrDigit(ch) || ch == '_'; + } + + private String parseId() { + int start = _position; + while (isIdLetter(nextChar())) ; + return new String(_input, start, --_position - start); + } + + private boolean isPunctuation(char ch) { + return "=;[]<>(){}+-/*!".indexOf(ch) != -1; + } + + private String parsePunctuation() { + int start = _position; + return new String(_input, start, 1); + } + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/Utils.java b/chapter10b/src/main/java/com/seaofnodes/simple/Utils.java new file mode 100644 index 00000000..4f2af769 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/Utils.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple; + +import java.util.ArrayList; + +public class Utils { + public static RuntimeException TODO() { return TODO("Not yet implemented"); } + public static RuntimeException TODO(String msg) { return new RuntimeException(msg); } + + /** + * Fast, constant-time, element removal. Does not preserve order + * + * @param array ArrayList to modify + * @param i element to be removed + * @return element removed + */ + public static E del(ArrayList array, int i) { + if ( i >= 0 && i < array.size() ) { + E tmp = array.get(i); + E last = array.removeLast(); + if (i < array.size()) array.set(i, last); + return tmp; + } + return null; + } + + /** + * 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( ArrayList ary, E x ) { + for( int i=0; i= 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/chapter10b/src/main/java/com/seaofnodes/simple/node/AddNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/AddNode.java new file mode 100644 index 00000000..de5044d7 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -0,0 +1,176 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.*; + +import java.util.BitSet; + +public class AddNode extends Node { + public AddNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Add"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("+"), visited); + return sb.append(")"); + } + + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if (i0.isConstant() && i1.isConstant()) + return TypeInteger.constant(i0.value()+i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize () { + Node lhs = in(1); + Node rhs = in(2); + Type t2 = rhs._type; + + // Add of 0. We do not check for (0+x) because this will already + // canonicalize to (x+0) + if( t2 instanceof TypeInteger i && i.value()==0 ) + return lhs; + + // Add of same to a multiply by 2 + if( lhs==rhs ) + return new MulNode(lhs,new ConstantNode(TypeInteger.constant(2)).peephole()); + + // Goal: a left-spine set of adds, with constants on the rhs (which then fold). + + // Move non-adds to RHS + if( !(lhs instanceof AddNode) && rhs instanceof AddNode ) + return swap12(); + + // x+(-y) becomes x-y + if( rhs instanceof MinusNode minus ) + return new SubNode(lhs,minus.in(1)); + + // Now we might see (add add non) or (add non non) or (add add add) but never (add non add) + + // Do we have x + (y + z) ? + // Swap to (x + y) + z + // Rotate (add add add) to remove the add on RHS + if( rhs instanceof AddNode add ) + return new AddNode(new AddNode(lhs,add.in(1)).peephole(), add.in(2)); + + // Now we might see (add add non) or (add non non) but never (add non add) nor (add add add) + if( !(lhs instanceof AddNode) ) + // Rotate; look for (add (phi cons) con/(phi cons)) + return spine_cmp(lhs,rhs,this) ? swap12() : phiCon(this,true); + + // Now we only see (add add non) + + // Dead data cycle; comes about from dead infinite loops. Do nothing, + // the loop will peep as dead after a bit. + if( lhs.in(1) == lhs ) + return null; + + // Do we have (x + con1) + con2? + // Replace with (x + (con1+con2) which then fold the constants + // lhs.in(2) is con1 here + // If lhs.in(2) is not a constant, we add ourselves as a dependency + // because if it later became a constant then we could make this + // transformation. + if( lhs.in(2).addDep(this)._type.isConstant() && rhs._type.isConstant() ) + return new AddNode(lhs.in(1),new AddNode(lhs.in(2),rhs).peephole()); + + + // 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 = phiCon(this,true); + if( phicon!=null ) return phicon; + + // Now we sort along the spine via rotates, to gather similar things together. + + // Do we rotate (x + y) + z + // into (x + z) + y ? + if( spine_cmp(lhs.in(2),rhs,this) ) + return new AddNode(new AddNode(lhs.in(1),rhs).peephole(),lhs.in(2)); + + return null; + } + + // Rotation is only valid for associative ops, e.g. Add, Mul, And, Or. + // Do we have ((phi cons)|(x + (phi cons)) + con|(phi cons)) ? + // Push constant up through the phi: x + (phi con0+con0 con1+con1...) + static Node phiCon(Node op, boolean rotate) { + Node lhs = op.in(1); + Node rhs = op.in(2); + // LHS is either a Phi of constants, or another op with Phi of constants + PhiNode lphi = pcon(lhs,op); + if( rotate && lphi==null && lhs.nIns() > 2 ) { + // Only valid to rotate constants if both are same associative ops + if( lhs.getClass() != op.getClass() ) return null; + lphi = pcon(lhs.in(2),op); // Will rotate with the Phi push + } + if( lphi==null ) return null; + + // RHS is a constant or a Phi of constants + if( !(rhs instanceof ConstantNode) && pcon(rhs,op)==null ) + return null; + + // If both are Phis, must be same Region + if( rhs instanceof PhiNode && lphi.in(0) != rhs.in(0) ) + return null; + + // Note that this is the exact reverse of Phi pulling a common op down + // to reduce total op-count. We don't get in an endless push-up + // push-down peephole cycle because the constants all fold first. + Node[] ns = new Node[lphi.nIns()]; + ns[0] = lphi.in(0); + // Push constant up through the phi: x + (phi con0+con0 con1+con1...) + for( int i=1; i hi._nid; + } + + @Override Node copy(Node lhs, Node rhs) { return new AddNode(lhs,rhs); } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/BoolNode.java new file mode 100644 index 00000000..bf0d3a84 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/BoolNode.java @@ -0,0 +1,76 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; + +import java.util.BitSet; + +abstract public class BoolNode extends Node { + + public BoolNode(Node lhs, Node rhs) { + super(null, lhs, rhs); + } + + abstract String op(); // String opcode name + + @Override + public String label() { return getClass().getSimpleName(); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append(op()), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if (i0.isConstant() && i1.isConstant()) + return TypeInteger.constant(doOp(i0.value(), i1.value()) ? 1 : 0); + } + return TypeInteger.BOT; + } + + abstract boolean doOp(long lhs, long rhs); + + @Override + public Node idealize() { + // Compare of same + if( in(1)==in(2) ) + return new ConstantNode(TypeInteger.constant(doOp(3,3)?1:0)); + + // Equals pushes constant to the right; 5==X becomes X==5. + if( this instanceof EQ ) { + if( !(in(2) instanceof ConstantNode) ) { + // con==noncon becomes noncon==con + if( in(1) instanceof ConstantNode ) + return new EQ(in(2),in(1)); + // Equals sorts by NID otherwise: non.high == non.low becomes non.low == non.high + else if( in(1)._nid > in(2)._nid ) + return new EQ(in(2),in(1)); + } + // Equals X==0 becomes a !X + if( (in(2)._type == TypeInteger.ZERO || in(2)._type == TypeMemPtr.NULLPTR) ) + return new NotNode(in(1)); + } + + // Do we have ((phi cons) cmp con) ? + // Do we have ((phi cons) cmp (phi cons)) ? + // Push the compare up through the phi: (phi con0 cmp con, con1 cmp con...) + Node phicon = AddNode.phiCon(this,this instanceof EQ); + if( phicon!=null ) return phicon; + + return null; + } + + public static class EQ extends BoolNode { public EQ(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "=="; } boolean doOp(long lhs, long rhs) { return lhs == rhs; } Node copy(Node lhs, Node rhs) { return new EQ(lhs,rhs); } } + public static class LT extends BoolNode { public LT(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "<" ; } boolean doOp(long lhs, long rhs) { return lhs < rhs; } Node copy(Node lhs, Node rhs) { return new LT(lhs,rhs); } } + public static class LE extends BoolNode { public LE(Node lhs, Node rhs) { super(lhs,rhs); } String op() { return "<="; } boolean doOp(long lhs, long rhs) { return lhs <= rhs; } Node copy(Node lhs, Node rhs) { return new LE(lhs,rhs); } } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java new file mode 100644 index 00000000..8d512f21 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java @@ -0,0 +1,237 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import java.util.BitSet; + +/** + * A Phi for bulk memory. It covers all aliases except {@link #_aliases}; + * those aliases must be represented by parallel precise MemPhis at the same + * program slice. + */ +public class BulkMemPhiNode extends PhiNode { + + private static final BitSet EMPTY = new BitSet(); + + // Excluded Aliases; empty means NO exclusions. + // Each split clones the exclusions; published sets are never mutated. + public final BitSet _aliases; + + public BulkMemPhiNode(String label, Node... inputs) { + this(label,EMPTY,inputs); + } + + private BulkMemPhiNode(String label, BitSet aliases, Node... inputs) { + super(label,TypeMem.BOT,inputs); + _aliases = aliases; + } + + public boolean isSplit(int alias) { return _aliases.get(alias); } + + @Override public String label() { return "BulkPhi"+_aliases; } + + @Override + public Type compute() { + Type t = super.compute(); + return t instanceof TypeMem mem ? mem : + t==Type.BOTTOM ? TypeMem.BOT : + TypeMem.TOP; + } + + @Override + public Node idealize() { + Node progress = super.idealize(); + if( progress != null ) return progress; + if( ((RegionNode)region()).inProgress() || region().nIns()<=1 ) + return null; // Input is in-progress + + // "Peek through" a MemMerge that covers this alias set on its default + for( int i=1; i= mmm.nIns() || mmm.in(alias)==null) ) + return false; + return true; + } + + + // True if the MemMerge has no precise slice still covered by this Phi. + private boolean canPeek(MemMergeNode mmm) { + return missingAlias(mmm,false)==0; + } + + // Return an alias required by an input but still covered by this Phi, or 0. + private int inputAlias(Node n) { + return switch(n) { + case ProjNode proj -> 0; + case ConstantNode con -> 0; + case MemMergeNode mmm -> missingAlias(mmm,false); + case MemOpNode mem -> unsplit(mem._alias); + case BulkMemPhiNode bulk -> missingAlias(bulk); + default -> throw new AssertionError("Unexpected memory input "+n); + }; + } + + // Return an alias required by a user but still covered by this Phi, or 0. + private int outputAlias(Node use) { + // This decision depends backwards on an immediate user's alias/set. + // Normal graph-neighbor enqueueing flows from defs to uses, so retain + // an explicit forward dependency to revisit this Phi when the user + // sharpens. + use.addDepForwards(this); + return switch(use) { + case ScopeNode scope -> 0; + case MemMergeNode mmm -> missingAlias(mmm,true); + case BulkMemPhiNode bulk -> missingAlias(bulk); + case MemOpNode mem -> unsplit(mem._alias); + case MemPhiNode phi -> unsplit(phi._alias); + default -> { + assert use instanceof ReturnNode + : "Unexpected bulk-memory user "+use; + yield 0; + } + }; + } + + // Alias zero/one is bulk, and an already excluded precise alias cannot + // trigger another split of this Phi. + private int unsplit(int alias) { + if( alias <= 1 ) return 0; + assert !_aliases.get(alias); + return alias; + } + + // First alias excluded by the neighboring bulk Phi but not by this one. + private int missingAlias(BulkMemPhiNode bulk) { + for( int alias = bulk._aliases.nextSetBit(0); alias >= 0; alias = bulk._aliases.nextSetBit(alias+1) ) + if( !_aliases.get(alias) ) + return alias; + return 0; + } + + // First precise MemMerge slice still covered by this Phi. When inspecting + // a user, ignore slots which point back to this Phi. + private int missingAlias(MemMergeNode mmm, boolean user) { + for( int alias=2; alias= 0; old = _aliases.nextSetBit(old+1) ) + { assert old != alias; mmm.alias(old,precisePhi(old));} + mmm.alias(alias,precise); + assert checkMerge(mmm); + return mmm; + } + + // Find or make the precise Phi parallel to this bulk Phi at the same merge. + private Node precisePhi(int alias) { + MemPhiNode mphi = _findPhi(alias); + if( mphi!=null ) return mphi; + + mphi = new MemPhiNode("$"+alias,alias); + mphi.addDef(region()); + // Due to cycles, must set before calling peephole + mphi.setType(TypeMem.make(alias)); + for( int i=1; i= 0; alias = bulk._aliases.nextSetBit(alias+1) ) + if( alias >= mmm.nIns() || mmm.in(alias)==null ) + return false; + return true; + } + + @Override public boolean eq(Node n) { + return _aliases.equals(((BulkMemPhiNode)n)._aliases) && super.eq(n); + } + + @Override int hash() { return _aliases.hashCode(); } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/CastNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/CastNode.java new file mode 100644 index 00000000..fec540f7 --- /dev/null +++ b/chapter10b/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/chapter10b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java new file mode 100644 index 00000000..d760c0df --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java @@ -0,0 +1,54 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; + +import java.util.BitSet; + +/** + * A Constant node represents a constant value. At present, the only constants + * that we allow are integer literals; therefore Constants contain an integer + * value. As we add other types of constants, we will refactor how we represent + * Constants. + *

+ * Constants have no semantic inputs. However, we set Start as an input to + * Constants to enable a forward graph walk. This edge carries no semantic + * meaning, and it is present solely to allow visitation. + *

+ * The Constant's value is the value stored in it. + */ +public class ConstantNode extends Node { + final Type _con; + public ConstantNode( Type type ) { + super(Parser.START); + _con = type; + } + + @Override + public String label() { return "#"+_con; } + + @Override + public String uniqueName() { return "Con_" + _nid; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return _con.print(sb); + } + + @Override public boolean isMultiTail() { return true; } + + @Override + public Type compute() { return _con; } + + @Override + public Node idealize() { return null; } + + @Override + boolean eq(Node n) { + ConstantNode con = (ConstantNode)n; // Contract + return _con==con._con; + } + + @Override + int hash() { return _con.hashCode(); } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/DivNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/DivNode.java new file mode 100644 index 00000000..6cee90ad --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/DivNode.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class DivNode extends Node { + public DivNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Div"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("/"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + if (t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1) { + if (i0.isConstant() && i1.isConstant()) + return i1.value() == 0 + ? TypeInteger.ZERO + : TypeInteger.constant(i0.value()/i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + // Div of 1. + if( in(2)._type.isConstant() && in(2)._type instanceof TypeInteger i && i.value()==1 ) + return in(1); + return null; + } + + @Override Node copy(Node lhs, Node rhs) { return new DivNode(lhs,rhs); } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/IfNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/IfNode.java new file mode 100644 index 00000000..1cf4ddaf --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/IfNode.java @@ -0,0 +1,62 @@ +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 + public String label() { return "If"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("if( "); + return in(1)._print0(sb, visited).append(" )"); + } + + @Override public boolean isCFG() { return true; } + @Override public boolean isMultiHead() { return true; } + + public Node ctrl() { return in(0); } + public Node pred() { return in(1); } + + @Override + public Type compute() { + // If the If node is not reachable then neither is any following Proj + if (ctrl()._type != Type.CONTROL && ctrl()._type != Type.BOTTOM ) + return TypeTuple.IF_NEITHER; + Node pred = pred(); + Type t = pred._type; + // High types mean NEITHER side is reachable. + // Wait until the type falls to decide which way to go. + if( t == Type.TOP || t == TypeInteger.TOP ) + return TypeTuple.IF_NEITHER; + // If constant is 0 then false branch is reachable + // Else true branch is reachable + if (t instanceof TypeInteger ti && ti.isConstant()) + return ti==TypeInteger.ZERO ? TypeTuple.IF_FALSE : TypeTuple.IF_TRUE; + + return TypeTuple.IF_BOTH; + } + + @Override + public Node idealize() { + // Hunt up the immediate dominator tree. If we find an identical if + // test on either the true or false branch, that side wins. + if( !pred()._type.isHighOrConst() ) + for( Node dom = idom(), prior=this; dom!=null; prior = dom, dom = dom.idom() ) + if( dom.addDep(this) instanceof IfNode iff && iff.pred().addDep(this)==pred() && prior instanceof ProjNode prj ) { + setDef(1,new ConstantNode(TypeInteger.make(true,prj._idx==0?1:0)).peephole()); + return this; + } + return null; + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/LoadNode.java new file mode 100644 index 00000000..55822722 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -0,0 +1,81 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.IterPeeps; +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 + StringBuilder _print1(StringBuilder sb, BitSet visited) { return sb.append(".").append(_name); } + + @Override + public Type compute() { + return _declaredType; + } + + @Override + public Node idealize() { + if( mem() instanceof MemMergeNode merge ) { + setDef(1,IterPeeps.add(merge.alias(_alias))); + return this; + } + + + // Simple Load-after-Store on same address. + if( mem() instanceof StoreNode st && + ptr() == st.ptr() && _alias==st._alias ) { // 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 MemPhiNode 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() && _alias==st1._alias; + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/LoopNode.java new file mode 100644 index 00000000..a0ba973e --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/LoopNode.java @@ -0,0 +1,31 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; + +import java.util.BitSet; + +public class LoopNode extends RegionNode { + + public LoopNode( Node entry ) { super(null,entry,null); } + + Node entry() { return in(1); } + Node back () { return in(2); } + + @Override + public String label() { return "Loop"; } + + @Override + public Type compute() { + if( inProgress() ) return Type.CONTROL; + return entry()._type; + } + + @Override + public Node idealize() { + return inProgress() ? null : super.idealize(); + } + + // Bypass Region idom, same as the default idom() using use entry in(1) instead of in(0) + @Override int idepth() { return _idepth!=0 ? _idepth : cacheIDepth(entry().idepth()+1); } + @Override Node idom() { return entry(); } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java new file mode 100644 index 00000000..d7fed402 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import java.util.BitSet; + +/** All memory: a default slice and explicit overrides for individual aliases. */ +public class MemMergeNode extends Node { + public MemMergeNode(Node bulk) { super(null,bulk); _type = TypeMem.BOT; } + public MemMergeNode(Node bulk, int alias, Node precise) { + this(bulk); + alias(alias,precise); + } + + public Node alias(int alias) { + Node n = alias < nIns() ? in(alias) : null; + assert n!=null || !(in(1) instanceof BulkMemPhiNode bulk) || !bulk.isSplit(alias); + return n==null ? in(1) : n; + } + + public void alias(int alias, Node mem) { + assert alias > 1; + while( nIns()<=alias ) addDef(null); + setDef(alias,mem); + } + + @Override public String label() { return "MemMerge"; } + @Override public boolean isMem() { return true; } + @Override public Type compute() { return TypeMem.BOT; } + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("MEM["); + for( int i=1; i=nIns() || in(i)==null) ) + alias(i,mem.in(i)); + IterPeeps.add(mem.in(1)); + setDef(1,mem.in(1)); + return this; + } + assert BulkMemPhiNode.checkMerge(this); + return progress ? this : null; + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java new file mode 100644 index 00000000..08e08bbf --- /dev/null +++ b/chapter10b/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/chapter10b/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java new file mode 100644 index 00000000..c806bede --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; + +/** A Phi for one field alias, parallel to a bulk Phi at the same Region. */ +public class MemPhiNode extends PhiNode { + public final int _alias; + + public MemPhiNode(String label, int alias, Node... inputs) { + super(label,TypeMem.make(alias),inputs); + assert alias > 1; + _alias = alias; + } + + @Override public String label() { return "MemPhi_"+_alias; } + + @Override public Type compute() { + assert BulkMemPhiNode.checkMem(this,_alias); + Type t = super.compute(); + return t==Type.TOP || t==TypeMem.TOP ? TypeMem.TOP : TypeMem.make(_alias); + } + + @Override public Node idealize() { + for( int i=1; i + * Generally fixed length, ordered, nulls allowed, no unused trailing space. + * Ordering is required because e.g. "a/b" is different from "b/a". + * The first input (offset 0) is often a {@link #isCFG} node. + */ + public final ArrayList _inputs; + + /** + * Outputs reference Nodes that are not null and have this Node as an + * input. These nodes are users of this node, thus these are def-use + * references to Nodes. + *

+ * Outputs directly match inputs, making a directed graph that can be + * walked in either direction. These outputs are typically used for + * efficient optimizations but otherwise have no semantics meaning. + */ + public final ArrayList _outputs; + + + /** + * Current computed type for this Node. This value changes as the graph + * changes and more knowledge is gained about the program. + */ + public Type _type; + + + /** + * A private Global Static mutable counter, for unique node id generation. + * To make the compiler multithreaded, this field will have to move into a TLS. + * Starting with value 1, to avoid bugs confusing node ID 0 with uninitialized values. + * */ + private static int UNIQUE_ID = 1; + public static int UID() { return UNIQUE_ID; } + + protected Node(Node... inputs) { + _nid = UNIQUE_ID++; // allocate unique dense ID + _inputs = new ArrayList<>(); + Collections.addAll(_inputs,inputs); + _outputs = new ArrayList<>(); + for( Node n : _inputs ) + if( n != null ) + n.addUse( this ); + } + + // Easy reading label for debugger, e.g. "Add" or "Region" or "EQ" + public abstract String label(); + + // Unique label for debugging, e.g. "Add12" or "Region30" or "EQ99" + public String uniqueName() { + return label() + _nid; + } + + // ------------------------------------------------------------------------ + + // Debugger Printing. + + // {@code toString} is what you get in the debugger. It has to print 1 + // line (because this is what a debugger typically displays by default) and + // has to be robust with broken graph/nodes. + @Override + public final String toString() { return print(); } + + // This is a *deep* print. This version will fail on cycles, which we will + // correct later when we can parse programs with loops. We print with a + // tik-tok style; the common _print0 calls the per-Node _print1, which + // calls back to _print0; + public final String print() { + return _print0(new StringBuilder(), new BitSet()).toString(); + } + + // This is the common print: check for repeats, check for DEAD and print + // "DEAD" else call the per-Node print1. + final StringBuilder _print0(StringBuilder sb, BitSet visited) { + if (visited.get(_nid) && !(this instanceof ConstantNode) ) + return sb.append(label()); + visited.set(_nid); + return isDead() + ? sb.append(uniqueName()).append(":DEAD") + : _print1(sb, visited); + } + // Every Node implements this; a partial-line recursive print + abstract StringBuilder _print1(StringBuilder sb, BitSet visited); + + public String p(int depth) { return IRPrinter.prettyPrint(this,depth); } + + public boolean isMultiHead() { return false; } + public boolean isMultiTail() { return false; } + + // ------------------------------------------------------------------------ + // Graph Node & Edge manipulation + + /** + * Gets the ith input node + * @param i Offset of the input node + * @return Input node or null + */ + public Node in(int i) { return _inputs.get(i); } + public Node out(int i) { return _outputs.get(i); } + + public int nIns() { return _inputs.size(); } + + public int nOuts() { return _outputs.size(); } + + public boolean isUnused() { return nOuts() == 0; } + + public boolean isCFG() { return false; } + + public boolean isMem() { return false; } + + /** + * Change a def into a Node. Keeps the edges correct, by removing + * the corresponding use->def edge. This may make the original + * def go dead. This function is co-recursive with {@link #kill}. + *

+ + * This method is the normal path for altering a Node, because it does the + * proper default edge maintenance. It also immediately kills + * Nodes that lose their last use; at times care must be taken to avoid + * killing Nodes that are being used without having an output Node. This + * definitely happens in the middle of recursive {@link #peephole} calls. + * + * @param idx which def to set + * @param new_def the new definition + * @return new_def for flow coding + */ + public Node setDef(int idx, Node new_def ) { + unlock(); + Node old_def = in(idx); + if( old_def == new_def ) return new_def; // No change + // If new def is not null, add the corresponding def->use edge + // This needs to happen before removing the old node's def->use edge as + // the new_def might get killed if the old node kills it recursively. + if( new_def != null ) + new_def.addUse(this); + if( old_def != null && // If the old def exists, remove a def->use edge + old_def.delUse(this) ) // If we removed the last use, the old def is now dead + 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; + } + + // Remove the numbered input, compressing the inputs in-place. This + // shuffles the order deterministically - which is suitable for Region and + // Phi, but not for every Node. If the def goes dead, it is recursively + // killed, which may include 'this' Node. + Node delDef(int idx) { + unlock(); + Node old_def = in(idx); + if( old_def != null && // If the old def exists, remove a def->use edge + old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + old_def.moveDepsToWorklist(); + Utils.del(_inputs, idx); + return this; + } + + /** + * Add a new def to an existing Node. Keep the edges correct by + * adding the corresponding def->use edge. + * + * @param new_def the new definition, appended to the end of existing definitions + * @return new_def for flow coding + */ + Node addDef(Node new_def) { + unlock(); + // Add use->def edge + _inputs.add(new_def); + // If new def is not null, add the corresponding def->use edge + if( new_def != null ) + new_def.addUse(this); + return new_def; + } + + // Breaks the edge invariants, used temporarily + protected void addUse(N n) { _outputs.add(n); } + + // Remove node 'use' from 'def's (i.e. our) output list, by compressing the list in-place. + // Return true if the output list is empty afterward. + // Error is 'use' does not exist; ok for 'use' to be null. + protected boolean delUse( Node use ) { + Utils.del(_outputs, Utils.find(_outputs, use)); + return _outputs.isEmpty(); + } + + // Shortcut for "popping" n nodes. A "pop" is basically a + // setDef(last,null) followed by lowering the nIns() count. + void popN(int n) { + unlock(); + for( int i=0; iuses, by setting all of its defs + * to null. This may recursively kill more Nodes, and is basically dead + * code elimination. + */ + public void kill( ) { + unlock(); + assert isUnused(); // Has no uses, so it is dead + _type=null; // Flag as dead + while( nIns()>0 ) { // Set all inputs to null, recursively killing unused Nodes + Node old_def = _inputs.removeLast(); + if( old_def != null ) { + IterPeeps.add(old_def);// Revisit neighbor because removed use + if( old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + } + } + assert isDead(); // Really dead now + } + + // Mostly used for asserts and printing. + public boolean isDead() { return isUnused() && nIns()==0 && _type==null; } + + // Shortcuts to stop DCE mid-parse + // Add bogus null use to keep node alive + public N keep() { addUse(null); return (N)this; } + // Remove bogus null. + 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 + public void subsume( Node nnn ) { + assert nnn!=this; + while( nOuts() > 0 ) { + Node n = _outputs.removeLast(); + n.unlock(); + int idx = Utils.find(n._inputs, this); + n._inputs.set(idx,nnn); + nnn.addUse(n); + } + kill(); + } + + // ------------------------------------------------------------------------ + // Graph-based optimizations + + /** + * Try to peephole at this node and return a better replacement Node. + * Always returns some not-null Node (often this). + */ + public final Node peephole() { + var obs = IterPeeps.midAssert() ? null : Parser.PARSER == null ? null : Parser.PARSER._obs; + if( obs != null ) obs.before(this); + Node n = peepholeOpt(); + Node rez = n == null ? this : deadCodeElim(n.peephole()); + if( obs != null ) obs.after(this, n == null ? null : rez, false); + return rez; + } + + /** + * Try to peephole at this node and return a better replacement Node if + * possible. We compute a {@link Type} and then check and replace: + *

    + *
  • if the Type {@link Type#isConstant}, we replace with a {@link ConstantNode}
  • + *
  • in a future chapter we will look for a + * Common Subexpression + * to eliminate.
  • + *
  • we ask the Node for a better replacement. The "better replacement" + * is things like {@code (1+2)} becomes {@code 3} and {@code (1+(x+2))} becomes + * {@code (x+(1+2))}. By canonicalizing expressions we fold common addressing + * math constants, remove algebraic identities and generally simplify the + * code.
  • + * + * Unlike peephole above, this explicitly returns null for no-change, or not-null + * for a better replacement (which can be this). + *
+ */ + public final Node peepholeOpt( ) { + ITER_CNT++; + // Compute initial or improved Type + Type old = setType(compute()); + + // Replace constant computations from non-constants with a constant node + if( !(this instanceof ConstantNode) && _type.isHighOrConst() ) + return new ConstantNode(_type).peepholeOpt(); + + // Global Value Numbering + if( _hash==0 ) { + Node n = GVN.get(this); // Will set _hash as a side effect + if( n==null ) + GVN.put(this,this); // Put in table now + else { + // Because of random worklist ordering, the two equal nodes + // might have different types. Because of monotonicity, both + // types are valid. To preserve monotonicity, the resulting + // shared Node has to have the best of both types. + n.setType(n._type.join(_type)); + _hash = 0; // Clear, since it never went in the table + return deadCodeElim(n);// Return previous; does Common Subexpression Elimination + } + } + + // Ask each node for a better replacement + Node n = idealize(); + if( n != null ) // Something changed + return n; // Report progress + + if( old==_type ) ITER_NOP_CNT++; + return old==_type ? null : this; // Report progress + } + public static int ITER_CNT, ITER_NOP_CNT; + + // m is the new Node, self is the old. + // Return 'm', which may have zero uses but is alive nonetheless. + // If self has zero uses (and is not 'm'), {@link #kill} self. + 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() && !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(). + m.keep(); // Keep m alive + kill(); // Kill self because replacing with 'm' + m.unkeep(); // Okay to peephole m + } + return m; + } + + /** + * This function needs to be + * Monotonic + * as it is part of a Monotone Analysis Framework. + * See for example this set of slides. + *

+ * For Chapter 2, all our Types are really integer constants, and so all + * the needed properties are trivially true, and we can ignore the high + * theory. Much later on, this will become important and allow us to do + * many fancy complex optimizations trivially... because theory. + *

+ * {@link #compute()} needs to be stand-alone, and cannot recursively call compute + * on its inputs, because programs are cyclic (have loops!) and this will just + * infinitely recurse until stack overflow. Instead, compute typically + * computes a new type from the {@link #_type} field of its inputs. + */ + public abstract Type compute(); + + // Set the type. Assert monotonic progress. + // If changing, add users to worklist. + public Type setType(Type type) { + Type old = _type; + assert old==null || type.isa(old); // Since _type not set, can just re-run this in assert in the debugger + if( old == type ) return old; + _type = type; // Set _type late for easier assert debugging + IterPeeps.addAll(_outputs); + moveDepsToWorklist(); + return old; + } + + + /** + * This function rewrites the current Node into a more "idealized" form. + * This is the bulk of our peephole rewrite rules, and we use this to + * e.g. turn arbitrary collections of adds and multiplies with mixed + * constants into a normal form that's easy for hardware to implement. + * Example: An array addressing expression: + *

   ary[idx+1]
+ * might turn into Sea-of-Nodes IR: + *
   (ary+12)+((idx+1) * 4)
+ * This expression can then be idealized into: + *
   ary + ((idx*4) + (12 + (1*4)))
+ * And more folding: + *
   ary + ((idx<<2) + 16)
+ * And during code-gen: + *
   MOV4 Rary,Ridx,16 // or some such hardware-specific notation 
+ *

+ * {@link #idealize} has a very specific calling convention: + *

    + *
  • If NO change is made, return {@code null} + *
  • If ANY change is made, return not-null; this can be {@code this} + *
  • The returned Node does NOT call {@link #peephole} on itself; the {@link #peephole} call will recursively peephole it. + *
  • Any NEW nodes that are not directly returned DO call {@link #peephole}. + *
+ *

+ * Examples: + * + * + * + * + * + *
before after return comment
{@code (x+5) }{@code (x+5) }{@code null }No change
{@code (5+x) }{@code (x+5) }{@code this }Swapped arguments
{@code ((x+1)+2)}{@code (x+(1+2))}{@code (x+_) }Returns 2 new Nodes
+ * + * The last entry deserves more discussion. The new Node {@code (1+2)} + * created in {@link #idealize} calls {@link #peephole} (which then folds + * into a constant). The other new Node {@code (x+3)} does not call + * peephole, because it is returned and peephole itself will recursively + * call peephole. + *

+ * Since idealize calls peephole and peephole calls idealize, you must be + * careful that all idealizations are monotonic: all transforms + * remove some feature, so that the set of available transforms always + * shrinks. If you don't, you risk an infinite peephole loop! + * + * @return Either a new or changed node, or null for no changes. + */ + public abstract Node idealize(); + + + // Some of the peephole rules get complex, and search further afield than + // just the nearest neighbor. These peepholes can fail the pattern match + // on a node some distance away, and if that node ever changes we should + // retry the peephole. Track a set of Nodes dependent on `this`, and + // revisit them if `this` changes. + ArrayList _deps; + public int nDeps() { return _deps == null ? 0 : _deps.size(); } + public Node dep(int idx) { return _deps.get(idx); } + + /** + * 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 ) { + return addDep(dep,false); + } + + // A bulk Phi inspects its users' partitions. Def-to-use propagation does + // not revisit that producer when an immediate user's partition changes. + Node addDepForwards(Node dep) { return addDep(dep,true); } + + private Node addDep(Node dep, boolean forwards) { + // Running peepholes during the big assert cannot have side effects + // like adding dependencies. + if( IterPeeps.midAssert() ) return this; + var obs = IterPeeps.midAssert() ? null : Parser.PARSER == null ? null : Parser.PARSER._obs; + if( obs != null ) obs.dep(this, dep); + if( _deps==null ) _deps = new ArrayList<>(); + if( Utils.find(_deps ,dep) != -1 ) return this; // Already on list + if( !forwards && Utils.find(_inputs,dep) != -1 ) return this; + if( !forwards && Utils.find(_outputs,dep)!= -1 ) return this; + _deps.add(dep); + return this; + } + + // Move the dependents onto a worklist, and clear for future dependents. + public void moveDepsToWorklist( ) { + if( _deps==null ) return; + IterPeeps.addAll(_deps); + _deps.clear(); + } + + + // Global Value Numbering. Hash over opcode and inputs; hits in this table + // are structurally equal. + public static final HashMap GVN = new HashMap<>(); + + // Two nodes are equal if they have the same inputs and the same "opcode" + // which means the same Java class, plus same internal parts. + @Override public final boolean equals( Object o ) { + if( o==this ) return true; + if( o.getClass() != getClass() ) return false; + Node n = (Node)o; + int len = _inputs.size(); + if( len != n._inputs.size() ) return false; + for( int i=0; i>13) ^ n._nid; + if( hash==0 ) hash = 0xDEADBEEF; // Bad hash, so use some junky thing + return (_hash=hash); + } + // Subclasses add extra hash info (such as ConstantNodes constant) + int hash() { return 0; } + + + // ------------------------------------------------------------------------ + // Peephole utilities + + // Swap inputs without letting either input go dead during the swap. + Node swap12() { + unlock(); // Hash is order dependent + Node tmp = in(1); + _inputs.set(1,in(2)); + _inputs.set(2,tmp); + return this; + } + + /** + * 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 char _idepth; // IDOM depth approx; Zero is unset; non-zero is cached legit + + // Find the lowest common ancestor in the current dominator tree. + Node domLCA(Node rhs) { + if( rhs==null ) return this; + Node lhs = this; + while( lhs != rhs ) { + if( lhs==null || rhs==null ) return null; + int comp = lhs.idepth() - rhs.idepth(); + if( comp >= 0 ) lhs = lhs.idom(); + if( comp <= 0 ) rhs = rhs.idom(); + } + return lhs; + } + + int idepth() { return _idepth!=0 ? _idepth : cacheIDepth(idom().idepth()+1); } + // Zero depth means uncached. Check before narrowing so overflow cannot wrap. + final int cacheIDepth(int depth) { + assert 0 <= depth && depth <= Character.MAX_VALUE : "Dominator depth exceeds 65535"; + return _idepth = (char)depth; + } + + + // 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. + */ + public static void reset() { + UNIQUE_ID = 1; + GVN.clear(); + ITER_CNT = ITER_NOP_CNT = 0; + } + + + // Utility to walk the entire graph applying a function; return the first + // not-null result. + private static final BitSet WVISIT = new BitSet(); + final public E walk( Function pred ) { + assert WVISIT.isEmpty(); + E rez = _walk(pred); + WVISIT.clear(); + return rez; + } + + private E _walk( Function pred ) { + if( WVISIT.get(_nid) ) return null; // Been there, done that + WVISIT.set(_nid); + E x = pred.apply(this); + if( x != null ) return x; + for( Node def : _inputs ) if( def != null && (x = def._walk(pred)) != null ) return x; + for( Node use : _outputs ) if( use != null && (x = use._walk(pred)) != null ) return x; + return null; + } + + /** + * Debugging utility to find a Node by index + */ + public Node find(int nid) { + return walk( n -> n._nid==nid ? n : null ); + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/NotNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/NotNode.java new file mode 100644 index 00000000..83141836 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/NotNode.java @@ -0,0 +1,42 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +public class NotNode extends Node { + public NotNode(Node in) { super(null, in); } + + @Override public String label() { return "Not"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("(!"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t0 = in(1)._type; + switch( t0 ) { + case TypeInteger i0: + return i0.isConstant() ? TypeInteger.constant(i0.value()==0 ? 1 : 0) : i0; + case TypeMemPtr p0: + // top->top, bot->bot, null->1, *void->0, not-null ptr->0, ptr/nil->bot + // If input in null then true + // If input is not null ptr then false + if( p0 == TypeMemPtr.TOP ) return TypeInteger.TOP; + if( p0 == TypeMemPtr.NULLPTR ) return TypeInteger.constant(1); + if( !p0._nil ) return TypeInteger.constant(0); + return TypeInteger.BOT; + case Type t: + if( t0.getClass() != Type.class ) + // Only doing NOT on ints and ptrs + throw Utils.TODO(); + return t0==Type.TOP ? TypeInteger.TOP : TypeInteger.BOT; + } + } + + @Override + public Node idealize() { return null; } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/PhiNode.java new file mode 100644 index 00000000..650edd6b --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/PhiNode.java @@ -0,0 +1,161 @@ +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; + +public class PhiNode extends Node { + + final String _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; } + + public static PhiNode make(String label, Type type, Node... inputs) { + return type instanceof TypeMem ? new BulkMemPhiNode(label,inputs) : new PhiNode(label,type,inputs); + } + + @Override public String label() { return "Phi_"+_label; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + if( !(region() instanceof RegionNode r) || r.inProgress() ) + sb.append("Z"); + sb.append("Phi("); + for( Node in : _inputs ) { + if (in == null) sb.append("____"); + else in._print0(sb, visited); + sb.append(","); + } + sb.setLength(sb.length()-1); + sb.append(")"); + return sb; + } + + 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; + // 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 ) + t = t.meet(in(i)._type); + return t; + } + + @Override + public Node idealize() { + if( !(region() instanceof RegionNode r ) ) + return in(1); // Input has collapse to e.g. starting control. + if( r.inProgress() || r.nIns()<=1 ) + return null; // Input is in-progress + + // If we have only a single unique input, become it. + Node live = singleUniqueInput(); + if (live != null) + return live; + + // Pull "down" a common data op. One less op in the world. One more + // Phi, but Phis do not make code. + // Phi(op(A,B),op(Q,R),op(X,Y)) becomes + // op(Phi(A,Q,X), Phi(B,R,Y)). + Node op = in(1); + if( !isMem() && op.nIns()==3 && op.in(0)==null && !op.isCFG() && same_op() ) { + Node[] lhss = new Node[nIns()]; + Node[] rhss = new Node[nIns()]; + lhss[0] = rhss[0] = in(0); // Set Region + for( int i=1; i1 && in(nIns()-1) == null; + } + + // Never equal if inProgress + @Override boolean eq( Node n ) { + return !inProgress(); + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java new file mode 100644 index 00000000..f13f3708 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.*; + +import java.util.BitSet; + +/** Keeps the return value and all preceding memory effects alive. */ +public class ReturnNode extends Node { + + public ReturnNode(Node ctrl, Node data, ScopeNode scope) { + super(ctrl, data, scope.lookup("$mem")); + } + + public Node ctrl() { return in(0); } + public Node expr() { return in(1); } + + @Override + public String label() { return "Return"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("return "); + expr()._print0(sb, visited); + return sb.append(";"); + } + + @Override public boolean isCFG() { return true; } + + @Override + public Type compute() { + return TypeTuple.make(ctrl()._type,expr()._type); + } + + @Override + public Node idealize() { + if( ctrl()._type==Type.XCONTROL ) + return ctrl(); + return null; + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java new file mode 100644 index 00000000..b931e862 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -0,0 +1,304 @@ +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.*; + +/** + * The Scope node is purely a parser helper - it tracks names to nodes with a + * stack of scopes. + */ +public class ScopeNode extends Node { + + /** + * The control is a name that binds to the currently active control + * node in the graph + */ + public static final String CTRL = "$ctrl"; + public static final String ARG0 = "arg"; + + /** + * Names for every input edge + */ + 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; + } + + + @Override public String label() { return "Scope"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("Scope[ "); + String[] names = reverseNames(); + for( int j=0; j syms : _scopes ) + for( String name : syms.keySet() ) + names[syms.get(name)] = name; + return names; + } + + @Override public Type compute() { return Type.BOTTOM; } + + @Override public Node idealize() { return null; } + + 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, 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); + } + /** + * Lookup a name in all scopes starting from most deeply nested. + * + * @param name Name to be looked up + * @see #update(String, Node, int) + */ + public Node lookup( String name ) { return update(name,null,_scopes.size()-1); } + /** + * If the name is present in any scope, then redefine else null + * + * @param name Name being redefined + * @param n The node to bind to the name + */ + 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. + * + * @param name Name whose binding is being updated or looked up + * @param n If null, do a lookup, else update binding + * @param nestingLevel The starting nesting level + * @return node being looked up, or the one that was updated + */ + private Node update( String name, Node n, int nestingLevel ) { + if( nestingLevel<0 ) return null; // Missed in all scopes, not found + var syms = _scopes.get(nestingLevel); // Get the symbol table for nesting level + var idx = syms.get(name); + if( idx == null ) return update(name,n,nestingLevel-1); // Not found in this scope, recursively look in parent scope + Node old = in(idx); + if( old instanceof ScopeNode loop ) { + // Lazy Phi! + old = loop.in(idx) instanceof PhiNode phi && loop.ctrl()==phi.region() + // Loop already has a real Phi, use it + ? loop.in(idx) + // 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,PhiNode.make(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); } + + /** + * The ctrl of a ScopeNode is always bound to the currently active + * control node in the graph, via a special name '$ctrl' that is not + * a valid identifier in the language grammar and hence cannot be + * referenced in Simple code. + * + * @param n The node to be bound to '$ctrl' + * + * @return Node that was bound + */ + public Node ctrl(Node n) { return setDef(0,n); } + + /** + * Duplicate a ScopeNode; including all levels, up to Nodes. So this is + * neither shallow (would dup the Scope but not the internal HashMap + * tables), nor deep (would dup the Scope, the HashMap tables, but then + * also the program Nodes). + *

+ * If the {@code loop} flag is set, the edges are filled in as the original + * Scope, as a indication of Lazy Phis at loop heads. The goal here is to + * not make Phis at loop heads for variables which are never touched in the + * loop body. + *

+ * The new Scope is a full-fledged Node with proper use<->def edges. + */ + public ScopeNode dup() { return dup(false); } + public ScopeNode dup(boolean loop) { + ScopeNode dup = new ScopeNode(); + // Our goals are: + // 1) duplicate the name bindings of the ScopeNode across all stack levels + // 2) Make the new ScopeNode a user of all the nodes bound + // 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 + for( int i=1; i { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); + if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); + return this; + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/StoreNode.java new file mode 100644 index 00000000..b752aad5 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -0,0 +1,74 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.IterPeeps; +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 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() { + if( mem() instanceof MemMergeNode merge ) { + setDef(1,IterPeeps.add(merge.alias(_alias))); + return this; + } + + + // 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() && _alias==st._alias && // 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/chapter10b/src/main/java/com/seaofnodes/simple/node/SubNode.java b/chapter10b/src/main/java/com/seaofnodes/simple/node/SubNode.java new file mode 100644 index 00000000..eeec5e1d --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/node/SubNode.java @@ -0,0 +1,63 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class SubNode extends Node { + public SubNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Sub"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("-"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==Type.TOP || t1==TypeInteger.TOP || + t2==Type.TOP || t2==TypeInteger.TOP ) + return TypeInteger.TOP; + // Sub of same is 0 + if( in(1)==in(2) ) + return TypeInteger.ZERO; + + if (t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1) { + if (i0.isConstant() && i1.isConstant()) + return TypeInteger.constant(i0.value()-i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Keep invalid operands for type checking. + if( t1 instanceof TypeInteger x && t2 instanceof TypeInteger y ) { + if( y.isConstant() && y.value()==0 ) return lhs; + if( x.isConstant() && x.value()==0 ) return new MinusNode(rhs); + } + + // x - (-y) is x+y + if( rhs instanceof MinusNode minus ) + return new AddNode(lhs,minus.in(1)); + + // (-x) - y is -(x+y) + if( lhs instanceof MinusNode minus ) + return new MinusNode(new AddNode(minus.in(1),rhs).peephole()); + + return null; + } + + @Override Node copy(Node lhs, Node rhs) { return new SubNode(lhs,rhs); } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter10b/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java new file mode 100644 index 00000000..5e21c852 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.graph.GraphAdapter; +import com.seaofnodes.graph.GraphSnapshot; +import com.seaofnodes.graph.GraphSnapshot.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMem; +import java.util.ArrayList; + +/** Chapter 10's view of the IR; browser, transport and layout are shared. */ +public class SimpleGraphAdapter extends GraphAdapter { + @Override protected boolean dead(Node n) { return n.isDead(); } + @Override protected int id(Node n) { return n._nid; } + @Override protected int nIns(Node n) { return n.nIns(); } + @Override protected Node in(Node n, int idx) { return n.in(idx); } + @Override protected int nOuts(Node n) { return n.nOuts(); } + @Override protected Node out(Node n, int idx) { return n.out(idx); } + @Override protected int nDeps(Node n) { return n.nDeps(); } + @Override protected Node dep(Node n, int idx) { return n.dep(idx); } + + private ArrayList edges(Node n) { + var edges = new ArrayList(); + String[] names = n instanceof ScopeNode scope && n.nIns() != 0 ? scope.reverseNames() : null; + for( int i = 0; i < n.nIns(); i++ ) { + String name = names == null || i >= names.length ? null : names[i]; + edges.add(new Edge(i, ref(n.in(i)), role(n, i), name)); + } + return edges; + } + + @Override protected GraphSnapshot.Node desc(Node n) { + Projection proj = n instanceof ProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; + String label = n.label(); + return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + } + + private Kind kind(Node n) { + if( n instanceof StopNode ) return Kind.STOP; + if( n instanceof ScopeNode ) return Kind.SCOPE; + if( n instanceof StartNode ) return Kind.CTRL; + if( n instanceof LoopNode ) return Kind.LOOP; + if( n instanceof RegionNode ) return Kind.REGION; + if( n instanceof PhiNode ) return Kind.PHI; + return n.isCFG() ? Kind.CTRL : isMem(n) ? Kind.MEM : Kind.DATA; + } + + private Role role(Node n, int i) { + if( n instanceof ScopeNode || n instanceof ConstantNode ) return Role.ASSOC; + if( n instanceof PhiNode ) return i == 0 ? Role.ASSOC : isMem(n) ? Role.MEM : Role.DATA; + if( n instanceof ProjNode ) return n.isCFG() ? Role.CTRL : isMem(n) ? Role.MEM : Role.DATA; + if( n instanceof RegionNode && i == 0 ) return Role.ASSOC; + if( i == 0 || n instanceof RegionNode || n instanceof StopNode ) return Role.CTRL; + if( i == 1 && n instanceof MemOpNode || i == 2 && n instanceof ReturnNode ) return Role.MEM; + Node def = n.in(i); + if( def != null && isMem(def) ) return Role.MEM; + return Role.DATA; + } + + private boolean isMem(Node n) { return n.isMem() || n._type instanceof TypeMem; } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter10b/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java new file mode 100644 index 00000000..1980659e --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.graph.GraphCapture; +import com.seaofnodes.graph.GraphViewer; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.Node; +import java.util.ArrayList; +import java.io.IOException; + +/** Chapter 10's compilation context; capture and event bookkeeping are shared. */ +public class SimpleGraphObserver extends GraphCapture { + private Parser _parser; + private Node _ret; + private String _phase; + public SimpleGraphObserver() { super(new SimpleGraphAdapter()); } + public static void main(String[] args) throws IOException { + GraphViewer.run(new SimpleGraphObserver()); + } + @Override protected void compile(String src) { + _parser = new Parser(src); + _parser._obs = this; + _phase = "Parse"; + _ret = _parser.parse(); + phase(_phase); + _phase = "Iter"; + com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); + phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); + } + @Override protected String phase() { return _phase; } + @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } + @Override protected void detach() { + if( _parser != null ) _parser._obs = null; + _parser = null; + _ret = null; + } + @Override protected Node scope() { return _parser._scope; } + @Override protected void roots(ArrayList roots) { + roots.add(Parser.START); + roots.add(_parser.STOP); + roots.addAll(_parser._xScopes); + roots.add(_ret); + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter10b/src/main/java/com/seaofnodes/simple/type/Field.java new file mode 100644 index 00000000..cdc58215 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/type/Field.java @@ -0,0 +1,59 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a field in a struct. This is not a Type in the type system. + */ +public class Field extends Type { + + // The pair {fieldName,type} uniquely identifies a field. + + // Field name + public final String _fname; + // Type of the field + public final Type _type; + + private Field(String fname, Type type ) { + super(TFLD); + _fname = fname; + _type = type; + } + public static Field make( String fname, Type type ) { + return new Field(fname,type).intern(); + } + + public static final Field TEST = make("test",TypeInteger.ZERO); + public static void gather(ArrayList 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/chapter10b/src/main/java/com/seaofnodes/simple/type/Type.java b/chapter10b/src/main/java/com/seaofnodes/simple/type/Type.java new file mode 100644 index 00000000..93e27c76 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/type/Type.java @@ -0,0 +1,187 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * These types are part of a Monotone Analysis Framework, + * @see see for example this set of slides. + *

+ * The types form a lattice; @see a symmetric complete bounded (ranked) lattice. + *

+ * This wild lattice theory will be needed later to allow us to easily beef up + * the analysis and optimization of the Simple compiler... but we don't need it + * now, just know that it is coming along in a later Chapter. + *

g + * One of the fun things here is that while the theory is deep and subtle, the + * actual implementation is darn near trivial and is generally really obvious + * what we're doing with it. Right now, it's just simple integer math to do + * simple constant folding e.g. 1+2 == 3 stuff. + */ + +public class Type { + static final HashMap INTERN = new HashMap<>(); + + // ---------------------------------------------------------- + // Simple types are implemented fully here. "Simple" means: the code and + // type hierarchy are simple, not that the Type is conceptually simple. + static final byte TBOT = 0; // Bottom (ALL) + static final byte TTOP = 1; // Top (ANY) + static final byte TCTRL = 2; // Ctrl flow bottom + static final byte TXCTRL = 3; // Ctrl flow top (mini-lattice: any-xctrl-ctrl-all) + 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; + + public boolean is_simple() { return _type < TSIMPLE; } + private static final String[] STRS = new String[]{"Bot","Top","Ctrl","~Ctrl"}; + protected Type(byte type) { _type = type; } + + public static final Type BOTTOM = new Type( TBOT ).intern(); // ALL + 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; } + + // Strict constant values, things on the lattice centerline. + // Excludes both high and low values + public boolean isConstant() { return false; } + + /** + * 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" + public T intern() { + T nnn = (T)INTERN.get(this); + if( nnn==null ) + INTERN.put(nnn=(T)this,this); + return nnn; + } + + private int _hash; // Hash cache; not-zero when set. + @Override + public final int hashCode() { + if( _hash!=0 ) return _hash; + _hash = hash(); + if( _hash==0 ) _hash = 0xDEADBEEF; // Bad hash from subclass; use some junk thing + return _hash; + } + // Override in subclasses + int hash() { return _type; } + + @Override + public final boolean equals( Object o ) { + if( o==this ) return true; + if( !(o instanceof Type t)) return false; + if( _type != t._type ) return false; + return eq(t); + } + // Overridden in subclasses; subclass can assume "this!=t" and java classes are same + boolean eq(Type t) { return true; } + + + // ---------------------------------------------------------- + public final Type meet(Type t) { + // Shortcut for the self case + if( t == this ) return this; + // Same-type is always safe in the subclasses + if( _type==t._type ) return xmeet(t); + // 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 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()'. + 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; + if( _type==TTOP || t._type==TBOT ) return t; + // 'this' is {TCTRL,TXCTRL} + if( !t.is_simple() ) return BOTTOM; + // 't' is {TCTRL,TXCTRL} + return _type==TCTRL || t._type==TCTRL ? CONTROL : XCONTROL; + } + + public Type dual() { + return switch( _type ) { + case TBOT -> TOP; + case TTOP -> BOTTOM; + case TCTRL -> XCONTROL; + case TXCTRL -> CONTROL; + default -> throw Utils.TODO(); // Should not reach here + }; + } + + // ---------------------------------------------------------- + // 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/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java new file mode 100644 index 00000000..8c77672b --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -0,0 +1,101 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +/** + * Integer Type + */ +public class TypeInteger extends Type { + + public final static TypeInteger TOP = make(false, 0); + public final static TypeInteger BOT = make(false, 1); + public final static TypeInteger ZERO= make(true , 0); + + public final boolean _is_con; + + /** + * The constant value or + * if not constant then 1=bottom, 0=top. + */ + public final long _con; + + private TypeInteger(boolean is_con, long con) { + super(TINT); + _is_con = is_con; + _con = con; + } + public static TypeInteger make(boolean is_con, long con) { + return new TypeInteger(is_con,con).intern(); + } + + 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"); + if( this==BOT ) return sb.append("IntBot"); + 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; } + + @Override + public boolean isConstant() { return _is_con; } + + public long value() { return _con; } + + @Override + public Type xmeet(Type other) { + // Invariant from caller: 'this' != 'other' and same class (TypeInteger) + TypeInteger i = (TypeInteger)other; // Contract + // BOT wins + if ( this==BOT ) return this; + if ( i ==BOT ) return i ; + // TOP loses + if ( i ==TOP ) return this; + if ( this==TOP ) return i ; + // Since both are constants, and are never equals (contract) unequals + // constants fall to bottom + return BOT; + } + + @Override + public Type dual() { + if( isConstant() ) return this; // Constants are a self-dual + 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 + public boolean eq( Type t ) { + TypeInteger i = (TypeInteger)t; // Contract + return _con==i._con && _is_con==i._is_con; + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeMem.java new file mode 100644 index 00000000..b6fc8824 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeMem.java @@ -0,0 +1,66 @@ +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) { + if( this==BOT ) return sb.append("MemBot"); + if( this==TOP ) return sb.append("MemTop"); + 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/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java new file mode 100644 index 00000000..6c874a93 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java @@ -0,0 +1,82 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +/** + * Represents a Pointer to memory. + * + * Null is generic pointer to non-existent memory. + * *void is a non-Null pointer to all possible structs. + * Pointers can be to specific struct types, or a union with Null. + * The distinguished *$BOT ptr represents union of *void and Null. + * The distinguished *$TOP ptr represents the dual of *$BOT. + */ +public class TypeMemPtr extends Type { + // A TypeMemPtr is pair (obj,nil) + // where obj is one of + // (null,TypeStruct). + // where nil is one of + // (true,false) meaning an explicit null is allowed or not + + public final TypeStruct _obj; // null, a TypeStruct, or sentinel TypeStruct.MANY + public final boolean _nil; + + private TypeMemPtr(TypeStruct obj, boolean nil) { + super(TMEMPTR); + _obj = obj; + _nil = nil; + } + public static TypeMemPtr make(TypeStruct obj, boolean nil) { return new TypeMemPtr(obj, nil).intern(); } + public static TypeMemPtr make(TypeStruct obj) { return TypeMemPtr.make(obj, false); } + + public static final TypeMemPtr BOT = make(TypeStruct.BOT,true); + public static final TypeMemPtr TOP = BOT.dual(); + public static final TypeMemPtr NULLPTR = make(TypeStruct.TOP,true); + public static final TypeMemPtr VOIDPTR = NULLPTR.dual(); // A bottom mix of not-null ptrs, like C's void* but not null + public static final TypeMemPtr TEST= make(TypeStruct.TEST,false); + public static void gather(ArrayList ts) { ts.add(NULLPTR); ts.add(BOT); ts.add(TEST); } + + @Override + Type xmeet(Type t) { + TypeMemPtr that = (TypeMemPtr) t; + return TypeMemPtr.make((TypeStruct)_obj.meet(that._obj), _nil | that._nil); + } + + @Override + public TypeMemPtr dual() { return TypeMemPtr.make(_obj==null ? null : _obj.dual(), !_nil); } + + @Override + public Type glb() { + if( _obj==null ) return BOT; + return make(_obj.glb(),true); + } + @Override public TypeMemPtr makeInit() { return NULLPTR; } + + @Override + int hash() { return (_obj==null ? 0xDEADBEEF : _obj.hashCode()) ^ (_nil ? 1024 : 0); } + + @Override + boolean eq(Type t) { + TypeMemPtr ptr = (TypeMemPtr)t; // Invariant + return _obj == ptr._obj && _nil == ptr._nil; + } + + // [void,name,MANY]*[,?] + @Override + public StringBuilder print(StringBuilder sb) { + if( this==BOT ) return sb.append("PtrBot"); + if( this==TOP ) return sb.append("PtrTop"); + if( this== NULLPTR) return sb.append("null"); + if( this== VOIDPTR) return sb.append("*void"); + return _obj.print(sb.append("*")).append(_nil ? "?" : ""); + } + + @Override public String str() { + if( this==BOT ) return "PtrBot"; + if( this==TOP ) return "PtrTop"; + if( this== NULLPTR) return "null"; + if( this== VOIDPTR) return "*void"; + return "*"+_obj.str()+(_nil ? "?" : ""); + } + +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java new file mode 100644 index 00000000..44741def --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -0,0 +1,127 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a struct type. + */ +public class TypeStruct extends Type { + + // For now in terms of the lattice + // a struct type just stays as is + + public final String _name; + public final Field[] _fields; + + private TypeStruct(String name, Field[] fields) { + super(Type.TSTRUCT); + _name = name; + _fields = fields; + } + + public static TypeStruct make(String name, Field[] fields) { return new TypeStruct(name, fields).intern(); } + public static TypeStruct make(String name, ArrayList 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) { + if( this==BOT ) return sb.append("StructBot"); + if( this==TOP ) return sb.append("StructTop"); + sb.append(_name).append(" {\n"); + for( Field f : _fields ) { + sb.append(" ").append(f._fname).append(":"); + f._type.print(sb); + sb.append(";\n"); + } + return sb.append("}"); + } + + @Override public String str() { + if( this==BOT ) return "StructBot"; + if( this==TOP ) return "StructTop"; + return _name; + } +} diff --git a/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeTuple.java b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeTuple.java new file mode 100644 index 00000000..0aa019a7 --- /dev/null +++ b/chapter10b/src/main/java/com/seaofnodes/simple/type/TypeTuple.java @@ -0,0 +1,88 @@ +package com.seaofnodes.simple.type; + +import java.util.ArrayList; + +public class TypeTuple extends Type { + + public final Type[] _types; + + 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 + Type xmeet(Type other) { + TypeTuple tt = (TypeTuple)other; // contract from xmeet + assert _types.length == tt._types.length; + Type[] ts = new Type[_types.length]; + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i].meet(tt._types[i]); + return make(ts); + } + + @Override public Type dual() { + Type[] ts = new Type[_types.length]; + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i].dual(); + 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("["); + for( Type t : _types ) + t.print(sb).append(","); + sb.setLength(sb.length()-1); + sb.append("]"); + 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}); + public static final TypeTuple IF_FALSE = make(new Type[]{Type.XCONTROL,Type. CONTROL}); + + @Override + int hash() { + int sum = 0; + for( Type type : _types ) sum ^= type.hashCode(); + return sum; + } + + @Override + boolean eq( Type t ) { + TypeTuple tt = (TypeTuple)t; // Contract + if( _types.length != tt._types.length ) return false; + for( int i=0; i<_types.length; i++ ) + if( _types[i]!=tt._types[i] ) + return false; + return true; + } + + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter01Test.java new file mode 100644 index 00000000..89882b2f --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter01Test.java @@ -0,0 +1,100 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter01Test { + + @Test + public void testSimpleProgram() { + Parser parser = new Parser("return 1;"); + StopNode stop = parser.parse(); + StartNode start = Parser.START; + ReturnNode ret = (ReturnNode)stop.in(0); + + assertTrue(ret.ctrl() instanceof ProjNode); + Node expr = ret.expr(); + if( expr instanceof ConstantNode con ) { + assertEquals(start,con.in(0)); + assertEquals(TypeInteger.constant(1), con._type); + } else { + fail(); + } + } + + @Test + public void testZero() { + Parser parser = new Parser("return 0;"); + parser.parse(); + StartNode start = Parser.START; + for( Node use : start._outputs ) + if( use instanceof ConstantNode con && con._type instanceof TypeInteger ) + assertEquals(TypeInteger.constant(0),con._type); + } + + @Test + public void testBad1() { + try { + new Parser("ret").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'ret'",e.getMessage()); + } + } + + @Test + public void testBad2() { + try { + new Parser("return 0123;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error: integer values cannot start with '0'",e.getMessage()); + } + } + + @Test + public void testNotBad3() { + // this test used to fail in chapter 1 + assertEquals("return 12;", new Parser("return --12;").parse().print()); + } + + @Test + public void testBad4() { + try { + new Parser("return 100").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected ;: ",e.getMessage()); + } + } + + @Test + public void testNotBad5() { + // this test used to fail in chapter 1 + assertEquals("return -100;", new Parser("return -100;").parse().print()); + } + + @Test + public void testBad6() { + try { + new Parser("return100").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'return100'",e.getMessage()); + } + } + + @Test + public void testBad7() { + try { + new Parser("return 1;}").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, unexpected }",e.getMessage()); + } + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter02Test.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter02Test.java new file mode 100644 index 00000000..a6075b75 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter02Test.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter02Test { + + @Test + public void testParseGrammar() { + Parser parser = new Parser("return 1+2*3+-5;"); + StopNode ret = parser.parse(); + assertEquals("return 2;", ret.print()); + } + + @Test + public void testAddPeephole() { + Parser parser = new Parser("return 1+2;"); + StopNode ret = parser.parse(); + assertEquals("return 3;", ret.print()); + } + + @Test + public void testSubPeephole() { + Parser parser = new Parser("return 1-2;"); + StopNode ret = parser.parse(); + assertEquals("return -1;", ret.print()); + } + + @Test + public void testMulPeephole() { + Parser parser = new Parser("return 2*3;"); + StopNode ret = parser.parse(); + assertEquals("return 6;", ret.print()); + } + + @Test + public void testDivPeephole() { + Parser parser = new Parser("return 6/3;"); + StopNode ret = parser.parse(); + assertEquals("return 2;", ret.print()); + } + + @Test + public void testMinusPeephole() { + Parser parser = new Parser("return 6/-3;"); + StopNode ret = parser.parse(); + assertEquals("return -2;", ret.print()); + } + + @Test + public void testExample() { + Parser parser = new Parser("return 1+2*3+-5;"); + StopNode ret = parser.parse(); + assertEquals("return 2;", ret.print()); + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter03Test.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter03Test.java new file mode 100644 index 00000000..609dff76 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter03Test.java @@ -0,0 +1,69 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter03Test { + + @Test + public void testVarDecl() { + Parser parser = new Parser("int a=1; return a;"); + StopNode ret = parser.parse(); + assertEquals("return 1;", ret.print()); + } + + @Test + public void testVarAdd() { + Parser parser = new Parser("int a=1; int b=2; return a+b;"); + StopNode ret = parser.parse(); + assertEquals("return 3;", ret.print()); + } + + @Test + public void testVarScope() { + Parser parser = new Parser("int a=1; int b=2; int c=0; { int b=3; c=a+b; } return c;"); + StopNode ret = parser.parse(); + assertEquals("return 4;", ret.print()); + } + + @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); "); + StopNode ret = parser.parse(); + assertEquals("return 8;", ret.print()); + } + + @Test + public void testSelfAssign() { + try { + new Parser("int a=a; return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'a'",e.getMessage()); + } + } + + @Test + public void testRedeclareVar() { + try { + new Parser("int a=1; int b=2; int c=0; int b=3; c=a+b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Redefining name 'b'",e.getMessage()); + } + } + + @Test + public void testBad1() { + try { + new Parser("int a=1; int b=2; int c=0; { int b=3; c=a+b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected }: ",e.getMessage()); + } + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter04Test.java new file mode 100644 index 00000000..f1603cc4 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter04Test.java @@ -0,0 +1,162 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter04Test { + + @Test + public void testPeephole() { + Parser parser = new Parser("return 1+arg+2; "); + StopNode ret = parser.parse(); + assertEquals("return (arg+3);", ret.print()); + } + + @Test + public void testPeephole2() { + Parser parser = new Parser("return (1+arg)+2;"); + StopNode ret = parser.parse(); + assertEquals("return (arg+3);", ret.print()); + } + + @Test + public void testAdd0() { + Parser parser = new Parser("return 0+arg;"); + StopNode ret = parser.parse(); + assertEquals("return arg;", ret.print()); + } + + @Test + public void testAddAddMul() { + Parser parser = new Parser("return arg+0+arg;"); + StopNode ret = parser.parse(); + assertEquals("return (arg*2);", ret.print()); + } + + @Test + public void testPeephole3() { + Parser parser = new Parser("return 1+arg+2+arg+3; "); + StopNode ret = parser.parse(); + assertEquals("return ((arg*2)+6);", ret.print()); + } + + @Test + public void testMul1() { + Parser parser = new Parser("return 1*arg;"); + StopNode ret = parser.parse(); + assertEquals("return arg;", ret.print()); + } + + @Test + public void testSub0() { + Parser parser = new Parser("return arg-0;"); + StopNode ret = parser.parse(); + assertEquals("return arg;", ret.print()); + } + + @Test + public void test0Sub() { + Parser parser = new Parser("return 0-arg;"); + StopNode ret = parser.parse(); + assertEquals("return (-arg);", ret.print()); + } + + @Test + public void testSubNeg() { + Parser parser = new Parser("return 3--arg;"); + StopNode ret = parser.parse(); + assertEquals("return (arg+3);", ret.print()); + } + + @Test + public void testNegSub() { + Parser parser = new Parser("return -arg-3;"); + StopNode ret = parser.parse(); + assertEquals("return (-(arg+3));", ret.print()); + } + + @Test + public void testVarArg() { + Parser parser = new Parser("return arg; "); + StopNode stop = parser.parse(); + ReturnNode ret = stop.ret(); + assertTrue(ret.in(0) instanceof ProjNode); + assertTrue(ret.in(1) instanceof ProjNode); + } + + @Test + public void testConstantArg() { + Parser parser = new Parser("return arg; ", TypeInteger.constant(2)); + StopNode ret = parser.parse(); + assertEquals("return 2;", ret.print()); + } + + @Test + public void testCompEq() { + Parser parser = new Parser("return 3==3; "); + StopNode ret = parser.parse(); + assertEquals("return 1;", ret.print()); + } + + @Test + public void testCompEq2() { + Parser parser = new Parser("return 3==4; "); + StopNode ret = parser.parse(); + assertEquals("return 0;", ret.print()); + } + + @Test + public void testCompNEq() { + Parser parser = new Parser("return 3!=3; "); + StopNode ret = parser.parse(); + assertEquals("return 0;", ret.print()); + } + + @Test + public void testCompNEq2() { + Parser parser = new Parser("return 3!=4; "); + StopNode ret = parser.parse(); + assertEquals("return 1;", ret.print()); + } + + @Test + public void testBug1() { + Parser parser = new Parser("int a=arg+1; int b=a; b=1; return a+2; "); + StopNode ret = parser.parse(); + assertEquals("return (arg+3);", ret.print()); + } + + @Test + public void testBug2() { + Parser parser = new Parser("int a=arg+1; a=a; return a; "); + StopNode ret = parser.parse(); + assertEquals("return (arg+1);", ret.print()); + } + + @Test + public void testBug3() { + try { + new Parser("inta=1; return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'inta'",e.getMessage()); + } + } + + @Test + public void testBug4() { + Parser parser = new Parser("return -arg;"); + StopNode ret = parser.parse(); + assertEquals("return (-arg);", ret.print()); + } + + @Test + public void testBug5() { + Parser parser = new Parser("return arg--2;"); + StopNode ret = parser.parse(); + assertEquals("return (arg--2);", ret.print()); + } +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter05Test.java new file mode 100644 index 00000000..aefb3a5c --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter05Test.java @@ -0,0 +1,213 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter05Test { + + @Test + public void testIfStmt() { + Parser parser = new Parser( +""" +int a = 1; +if (arg == 1) + a = arg+2; +else { + a = arg-3; + +} + +return a;"""); + StopNode ret = parser.parse().iterate(); + assertEquals("return Phi(Region19,(arg+2),(arg-3));", ret.toString()); + } + + @Test + public void testTest() { + Parser parser = new Parser( +""" +int c = 3; +int b = 2; +if (arg == 1) { + b = 3; + c = 4; +} +return c;""", TypeInteger.BOT); + StopNode ret = parser.parse().iterate(); + assertEquals("return Phi(Region17,4,3);", ret.toString()); + } + + @Test + public void testReturn2() { + Parser parser = new Parser( +""" +if( arg==1 ) + return 3; +else + return 4; +""", TypeInteger.BOT); + StopNode stop = parser.parse(); + assertEquals("Stop[ return 3; return 4; ]", stop.toString()); + } + + @Test + public void testIfMergeB() { + Parser parser = new Parser( +""" +int a=arg+1; +int b=0; +if( arg==1 ) + b=a; +else + b=a+1; +return a+b;"""); + StopNode ret = parser.parse().iterate(); + assertEquals("return ((arg*2)+Phi(Region22,2,3));", ret.toString()); + } + + @Test + public void testIfMerge2() { + Parser parser = new Parser( +""" +int a=arg+1; +int b=arg+2; +if( arg==1 ) + b=b+a; +else + a=b+1; +return a+b;"""); + StopNode ret = parser.parse().iterate(); + assertEquals("return ((Phi(Region33,(arg*2),arg)+arg)+Phi(Region,4,5));", ret.toString()); + } + + @Test + public void testMerge3() { + Parser parser = new Parser( +""" +int a=1; +if( arg==1 ) + if( arg==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +""", TypeInteger.BOT); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Region37,Phi(Region23,2,3),Phi(Region35,4,5));", stop.toString()); + } + + @Test + public void testMerge4() { + Parser parser = new Parser( +""" +int a=0; +int b=0; +if( arg ) + a=1; +if( arg==0 ) + b=2; +return arg+a+b; +""", TypeInteger.BOT); + StopNode stop = parser.parse(); + assertEquals("return ((arg+Phi(Region14,1,0))+Phi(Region29,2,0));", stop.toString()); + } + + @Test + public void testMerge5() { + Parser parser = new Parser( +""" +int a=arg==2; +if( arg==1 ) +{ + a=arg==3; +} +return a;"""); + StopNode ret = parser.parse().iterate(); + assertEquals("return (arg==Phi(Region17,3,2));", ret.toString()); + } + + @Test + public void testTrue() { + StopNode stop = new Parser("return true;").parse(); + assertEquals("return 1;",stop.toString()); + } + + @Test + public void testHalfDef() { + try { + new Parser("if( arg==1 ) int b=2; return b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Cannot define a new name on one arm of an if",e.getMessage()); + } + } + + @Test + public void testHalfDef2() { + try { + new Parser("if( arg==1 ) { int b=2; } else { int b=3; } return b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'b'",e.getMessage()); + } + } + + @Test + public void testRegress1() { + try { + new Parser("if(arg==2) int a=1; else int b=2; return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Cannot define a new name on one arm of an if",e.getMessage()); + } + } + + + @Test + public void testBadNum() { + try { + new Parser("return 1-;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected an identifier or expression: ;",e.getMessage()); + } + } + + @Test + public void testKeyword1() { + try { + new Parser("int true=0; return true;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Expected an identifier, found 'true'",e.getMessage()); + } + } + + @Test + public void testKeyword2() { + try { + new Parser("int else=arg; if(else) else=2; else else=1; return else;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Expected an identifier, found 'else'",e.getMessage()); + } + } + + @Test + public void testKeyword3() { + try { + new Parser("int a=1; ififif(arg)inta=2;return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'ififif'",e.getMessage()); + } + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter06Test.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter06Test.java new file mode 100644 index 00000000..920e26e3 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter06Test.java @@ -0,0 +1,294 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter06Test { + + @Test + public void testPeepholeReturn() { + Parser parser = new Parser( +""" +if( true ) return 2; +return 1; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 2;", stop.toString()); + assertTrue(stop.ret().ctrl() instanceof ProjNode); + } + + @Test + public void testPeepholeRotate() { + Parser parser = new Parser( +""" +int a = 1; +if (arg) + a = 2; +return (arg < a) < 3; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return ((arg 4 ) + break; +} +return arg; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return arg;", stop.toString()); + } + + @Test + public void testRegress2() { + Parser parser = new Parser("if(1) return 0; else while(arg>--arg) arg=arg+1; return 0;"); + StopNode stop = parser.parse().iterate(); + assertEquals("return 0;", stop.toString()); + } + + @Test + public void testBreakOutsideLoop() { + try { + new Parser(""" +if(arg <= 10) { + break; + arg = arg + 1; +} +return arg; +""").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("No active loop for a break or continue",e.getMessage()); + } + } + + @Test + public void testRegress3() { + Parser parser = new Parser(""" +while(arg < 10) { + break; +} +return arg; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return arg;", stop.toString()); + } + + @Test + public void testRegress4() { + Parser parser = new Parser(""" +int a = 1; +while(arg < 10) { + a = a + 1; + if (arg > 2) { + int a = 17; + break; + } +} +return a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Region29,Phi(Loop8,1,(Phi_a+1)),Add);", stop.toString()); + assertTrue(stop.ret().ctrl() instanceof RegionNode); + } + + @Test + public void testRegress5() { + Parser parser = new Parser(""" +int a = 1; +while(1) { + a = a + 1; + if (a<10) continue; + break; +} +return a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return (Phi(Loop8,1,Add)+1);", stop.toString()); + assertTrue(stop.ret().ctrl() instanceof ProjNode prj && prj._idx==1 ); + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter09Test.java new file mode 100644 index 00000000..1b1920e8 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter09Test.java @@ -0,0 +1,301 @@ +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; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.Ignore; + +import static org.junit.Assert.*; + +public class Chapter09Test { + + @Test + @Ignore + public void testJig() { + Parser parser = new Parser(""" +int v0=0; +arg=0; +while(v0) { + while(1) if(arg*arg*0==0) {} + while(0) {} + arg=1; +} +return 0; + """); + StopNode stop = parser.parse().iterate(); + } + + @Test + public void testGVN1() { + Parser parser = new Parser( + """ +int x = arg + arg; +if(arg < 10) { + return arg + arg; +} +else { + x = x + 1; +} +return x; + """); + StopNode stop = parser.parse(); + assertEquals("Stop[ return (arg*2); return (Mul+1); ]", stop.toString()); + Assert.assertEquals(2L, Evaluator.evaluate(stop, 1)); + Assert.assertEquals(23L, Evaluator.evaluate(stop, 11)); + } + + @Test + public void testGVN2() { + Parser parser = new Parser( + """ +return arg*arg-arg*arg; + """); + StopNode stop = parser.parse(); + assertEquals("return 0;", stop.toString()); + Assert.assertEquals(0L, Evaluator.evaluate(stop, 1)); + } + + @Test + public void testWorklist1() { + Parser parser = new Parser( + """ +int step = 1; +while (arg < 10) { + arg = arg + step + 1; +} +return arg; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop8,arg,(Phi_arg+2));", stop.toString()); + Assert.assertEquals(11L, Evaluator.evaluate(stop, 1)); + } + + @Test + public void testWorklist2() { + Parser parser = new Parser( + """ +int cond = 0; +int one = 1; +while (arg < 10) { + if (cond) one = 2; + arg = arg + one*3 + 1; +} +return arg; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop9,arg,(Phi_arg+4));", stop.toString()); + Assert.assertEquals(13L, Evaluator.evaluate(stop, 1)); + } + + @Test + public void testWorklist3() { + Parser parser = new Parser( + """ +int v1 = 0; +int v2 = 0; +int v3 = 0; +int v4 = 0; +int v5 = 0; +int v6 = 0; +int v7 = 0; +int v8 = 0; +while (arg) { + if (v1) v2 = 1; + if (v2) v3 = 1; + if (v3) v4 = 1; + if (v4) v5 = 1; + if (v5) v6 = 1; + if (v6) v7 = 1; + if (v7) v8 = 1; + arg = arg + v8 + 1; +} +return arg; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop15,arg,(Phi_arg+1));", stop.toString()); + } + + @Test + public void testRegionPeepBug() { + Parser parser = new Parser( + """ +int v0=0; +int v1=0; +while(v1+arg) { + arg=0; + int v2=v0; + while(arg+1) {} + v0=1; + v1=v2; +} + """); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testWhile0() { + Parser parser = new Parser("while(0) continue; if(0) arg=0;"); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testWhile1() { + Parser parser = new Parser(""" +if(0) while(0) { + int arg=arg; + while(0) {} +} +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + + @Test + public void testPrecedence() { + Parser parser = new Parser("return 3-1+2;"); + StopNode stop = parser.parse().iterate(); + assertEquals("return 4;", stop.toString()); + } + + @Test + public void testSwap2() { + Parser parser = new Parser("return 1+(1+1);"); + StopNode stop = parser.parse().iterate(); + assertEquals("return 3;", stop.toString()); + } + + + @Test + public void testFuzz0() { + Parser parser = new Parser(""" +int one = 1; +int a = 0; +int zero = 0; +while(arg) { + a = -(one + a + 2); + arg = arg + 1; + one = one + zero; +} +return a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop10,0,(-(Phi_a+3)));", stop.toString()); + } + + @Test + public void testFuzz1() { + Parser parser = new Parser(""" +while(1) {} +while(arg) break; +while(arg) arg=0; +arg=0; +int v0=0!=0<-0; +return -0+0+0; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + + @Test + public void testFuzz2() { + Parser parser = new Parser("return 0+-0;"); + StopNode stop = parser.parse().iterate(); + assertEquals("return 0;", stop.toString()); + } + + @Test + public void testFuzz3() { + Parser parser = new Parser("int v0=0; while(0==69) while(v0) return 0;"); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz4() { + Parser parser = new Parser(""" +while(1) { + arg=0<=0; + if(1<0) while(arg==-0) arg=arg-arg; +} +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz5() { + Parser parser = new Parser(""" +{ + int v0=0; + while(1) + int v1=0--0; + while(v0) + break; + while(-v0) { + while(0+0+v0) continue; + break; + } + if(-0!=-0+0+v0) while(0+0+0+0) + break; +} +return 0!=0; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz6() { + Parser parser = new Parser(""" +int v0=0; +while(0==1) while(v0) + v0=1+v0; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz7() { + Parser parser = new Parser(""" +while(1) {} +int v0=0; +while(v0) + {} +int v1=0; +while(1) + v1=1; +return v1+v0; + """); + StopNode stop = parser.parse().iterate(); + assertEquals("Stop[ ]", stop.toString()); + } + + @Test + public void testFuzz8() { + Parser parser = new Parser("while(arg) arg = arg - 1; return arg;"); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop7,arg,(Phi_arg-1));", stop.toString()); + } + + @Test + public void testMeet() { + Type t1 = Type.TOP; + Type t2 = TypeInteger.TOP; + Assert.assertEquals(TypeInteger.TOP, t1.meet(t2)); + Assert.assertEquals(TypeInteger.TOP, t2.meet(t1)); + t1 = Type.BOTTOM; + t2 = TypeInteger.BOT; + Assert.assertEquals(Type.BOTTOM, t1.meet(t2)); + Assert.assertEquals(Type.BOTTOM, t2.meet(t1)); + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter10Test.java new file mode 100644 index 00000000..b59eee93 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -0,0 +1,522 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; +import com.seaofnodes.simple.type.*; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter10Test { + @Test public void testDeadReferenceReturn() { + for( String body : new String[] { + "return new S; return 0;", + "if(1) return new S; else return 0;" + } ) { + String src = "struct S { int x; }; " + body; + var stop = new Parser(src).parse().iterate(); + org.junit.Assert.assertTrue(body, + stop.ret().expr()._type instanceof com.seaofnodes.simple.type.TypeMemPtr); + } + } + + + + // Issue #246: null-check guards start in Chapter 10; arrays arrive in Chapter 15. + private static final String NULLABLE_POINT_SOURCE = """ + struct Point { int x; }; + Point point = new Point; + point.x = 42; + Point? p = null; + if (arg) p = point; + """; + + @Test + public void testNullGuards() { + for( String body : new String[] { + "if (p != null) return p.x; return -1;", + "if (null != p) return p.x; return -1;", + "if (!!!!p) return p.x; return -1;", + "if (!!!p) return -1; else return p.x;", + "int b = !!p; if (b) return p.x + b - 1; return -1;" + } ) { + StopNode stop = new Parser(NULLABLE_POINT_SOURCE+body).parse().iterate(); + assertEquals(body,-1L,com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,0)); + assertEquals(body,42L,com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,1)); + } + } + + @Test + public void testNullGuardErrors() { + for( String body : new String[] { + "return p.x;", + "if (p == null) return p.x; return -1;", + "if (!!point) return p.x; return -1;", + "if (!!p) { int x = p.x; } return p.x;" + } ) { + try { + new Parser(NULLABLE_POINT_SOURCE+body).parse().iterate(); + fail(body); + } catch( RuntimeException e ) { + // Known null is rejected during parsing in these chapters. + String expected = body.startsWith("if (p == null)") + ? "Accessing unknown field 'x' from 'null'" : "Might be null accessing 'x'"; + assertEquals(body,expected,e.getMessage()); + } + } + } + + @Test + public void testFuzzer() { + Parser parser = new Parser( +""" +int a = arg/3; +int b = arg*5; +int x = arg*7; +int y = arg/11; +int p; int g; int h; +if( (arg/13)==0 ) { + p = x + y; + g = x; + h = y; +} else { + p = a + b; + g = a; + h = b; +} +int r = g+h; +return p-r; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 0;", stop.toString()); + } + + @Test + public void testStruct() { + Parser parser = new Parser(""" +struct Bar { + int a; + int b; +} +struct Foo { + int x; +} +Foo? foo = null; +Bar bar = new Bar; +bar.a = 1; +bar.a = 2; +return bar.a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return 2;", stop.toString()); + } + + @Test + public void testExample() { + Parser parser = new Parser(""" +struct Vector2D { int x; int y; } +Vector2D v = new Vector2D; +v.x = 1; +if (arg) + v.y = 2; +else + v.y = 3; +return v; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return new Vector2D;", stop.toString()); + } + + @Test + public void testBug() { + Parser parser = new Parser(""" +struct s0 { + int v0; +} +s0? v1=null; +int v3=v1.zAicm; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'zAicm' from 'null'",e.getMessage()); } + } + + @Test + public void testBug2() { + Parser parser = new Parser(""" +struct s0 { int v0; } +arg=0+new s0.0; +"""); + try { parser.parse(); fail(); } + catch( Exception e ) { assertEquals("Expected an identifier, found 'null'",e.getMessage()); } + } + + @Test + public void testLoop() { + Parser parser = new Parser(""" +struct Bar { int a; } +Bar bar = new Bar; +while (arg) { + bar.a = bar.a + 2; + arg = arg + 1; +} +return bar.a; +"""); + StopNode stop = parser.parse().iterate(); + assertEquals("return Phi(Loop11,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(Region16,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(Region34,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(Loop16,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(Loop11,new s0,Phi(Region34,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(Loop15,new s0,Phi(Region35,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(Loop11,new s0,Phi(Region32,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 0) { + s.x = s.x + 1; + int j = 3; + while (j > 0) { + j = j - 1; + if (j == 1) continue; + s.y = s.y + arg; + if (arg == 2) break; + s.z = s.z + 1; + } + arg = arg - 1; + } + return s; + """; + + @Test public void testMemoryAcrossNestedLoops() { + var stop = new Parser(NESTED_MEMORY).parse().iterate(); + for (int arg=0; arg<7; arg++) { + var obj = (com.seaofnodes.simple.evaluator.Evaluator.Obj) + com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,arg); + org.junit.Assert.assertArrayEquals(new Object[] { + 5L+arg, 7L+arg*(arg+1)-(arg>=2 ? 2 : 0), 11L+2*arg-(arg>=2 ? 2 : 0) + }, obj.fields()); + } + } + + @Test public void testMemoryAtEarlyReturns() { + var stop = new Parser(""" + struct S { int x; int y; int z; } + S s = new S; + s.x = 3; s.y = 5; s.z = 7; + if (arg) { s.x = 11; s.z = 13; return s; } + s.y = 17; + return s; + """).parse().iterate(); + for (int arg=0; arg<2; arg++) { + var obj = (com.seaofnodes.simple.evaluator.Evaluator.Obj) + com.seaofnodes.simple.evaluator.Evaluator.evaluate(stop,arg); + org.junit.Assert.assertArrayEquals(arg==0 ? new Object[]{3L,17L,7L} : new Object[]{11L,5L,13L}, obj.fields()); + } + } +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/Chapter10bTest.java b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter10bTest.java new file mode 100644 index 00000000..6f094cfb --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/Chapter10bTest.java @@ -0,0 +1,64 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.evaluator.Evaluator; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter10bTest { + @Test public void testIndependentField() { + var stop = new Parser(""" + struct S { int x; int y; } + S s = new S; + s.x = 42; + while (arg > 0) { s.y = s.y + arg; arg = arg - 1; } + return s.x; + """).parse().iterate(); + assertEquals("return 42;",stop.toString()); + assertEquals(42L,Evaluator.evaluate(stop,5)); + assertNull(stop.walk(n -> n instanceof LoadNode ? n : null)); + } + + @Test public void testParallelMemoryPhis() { + var stop = new Parser(""" + struct S { int x; int y; int z; } + S a = new S; S b = new S; + S p = a; if (arg) p = b; + while (arg > 0) { + a.x = p.x + arg; + a.y = p.y + 2; + a.z = p.z + 3; + arg = arg - 1; + } + return a; + """).parse().iterate(); + int[] phis = {0}; + stop.walk(n -> { + if (n instanceof MemPhiNode) phis[0]++; + if (n instanceof MemMergeNode m && m.in(1) instanceof BulkMemPhiNode b) + for(int a=b._aliases.nextSetBit(0); a>=0; a=b._aliases.nextSetBit(a+1)) + assertTrue(a=3); + assertArrayEquals(new Object[]{1L,2L,3L},((Evaluator.Obj)Evaluator.evaluate(stop,4)).fields()); + } + + @Test public void testLateSliceWorklist() throws ReflectiveOperationException { + // Seed 97 exposed a store selecting a bulk predecessor without queuing + // it. Keep both the worklist assertion and the final heap check. + for (int seed : new int[]{0,97,123,456}) { + var parser = new Parser(Chapter10Test.NESTED_MEMORY); + var field = IterPeeps.class.getDeclaredField("WORK"); + field.setAccessible(true); + Object work = field.get(null); + field = work.getClass().getDeclaredField("_R"); + field.setAccessible(true); + ((java.util.Random)field.get(work)).setSeed(seed); + var stop = parser.parse().iterate(); + assertArrayEquals(new Object[]{9L,25L,17L},((Evaluator.Obj)Evaluator.evaluate(stop,4)).fields()); + } + } +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/FuzzerWrap.java b/chapter10b/src/test/java/com/seaofnodes/simple/FuzzerWrap.java new file mode 100644 index 00000000..019140ad --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/FuzzerWrap.java @@ -0,0 +1,50 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.fuzzer.Fuzzer; +import org.junit.Test; +import org.junit.Ignore; + + +import static org.junit.Assert.assertTrue; + +/** + * Normal tests run chapter-local regression seeds. Open failures and exploratory + * fuzzing are opt-in (run directly in the IDE or temporarily remove Ignore). + * Seeds depend on this chapter's generator; do not copy them between chapters. + */ +public class FuzzerWrap { + + private static final long[] REGRESSION_SEEDS = { + }; + + private static final long[] OPEN_FAILING_SEEDS = { + }; + + @Test public void fuzzPeepsRegression () { fuzzPeepsSeeds(REGRESSION_SEEDS); } + @Test @Ignore public void fuzzPeepsOpenFailures() { fuzzPeepsSeeds(OPEN_FAILING_SEEDS); } + + private static void fuzzPeepsSeeds(long... seeds) { + var fuzzer = new Fuzzer(); + for (long seed : seeds) + fuzzer.fuzzPeepsRegression(seed); + } + + @Test + @Ignore + public void fuzzPeepsLarge() { + var fuzzer = new Fuzzer(); + for (int i=0; i<1000000; i++) + fuzzer.fuzzPeeps(i); + assertTrue(fuzzer.noExceptions()); + } + + + @Test + @Ignore + public void fuzzPeepTiming() { + var fuzzer = new Fuzzer(); + int max_nid=0; + for (int i=0; i<1000000; i++) + max_nid = fuzzer.fuzzPeepTiming(i, max_nid); + } +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/SchedulerTest.java b/chapter10b/src/test/java/com/seaofnodes/simple/SchedulerTest.java new file mode 100644 index 00000000..ad595822 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/SchedulerTest.java @@ -0,0 +1,73 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.evaluator.Evaluator; +import com.seaofnodes.simple.node.StopNode; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SchedulerTest { + + private static void assertObj(Object obj, String name, Object... fields) { + assertEquals(Evaluator.Obj.class, obj.getClass()); + var o = (Evaluator.Obj)obj; + assertEquals(name, o.struct()._name); + Assert.assertArrayEquals(fields, o.fields()); + } + + @Test + @Ignore + public void testJig() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v=new S; +v.f = 2; +int i=v.f; +if (arg) v.f=1; +return i; +"""); + StopNode stop = parser.parse();//.iterate(); + var eval = new Evaluator(stop); + assertEquals(2L, eval.evaluate(1, 10)); + } + + @Test + public void testStoreInIf() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v0=new S; +if(arg) v0.f=1; +return v0; +"""); + StopNode stop = parser.parse().iterate(); + var eval = new Evaluator(stop); + assertObj(eval.evaluate(0, 10), "S", 0L); + assertObj(eval.evaluate(1, 10), "S", 1L); + } + + @Test + public void testStoreInIf2() { + Parser parser = new Parser(""" +struct S { + int f; +} +S v=new S; +v.f = 2; +int i=new S.f; +i=v.f; +if (arg) v.f=1; +return i; +"""); + StopNode stop = parser.parse().iterate(); + var eval = new Evaluator(stop); + assertEquals(2L, eval.evaluate(0, 10)); + assertEquals(2L, eval.evaluate(1, 10)); + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/TypeTest.java b/chapter10b/src/test/java/com/seaofnodes/simple/TypeTest.java new file mode 100644 index 00000000..f9c580cc --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -0,0 +1,145 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.type.*; +import org.junit.Assert; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class TypeTest { + + // Test basic properties and GLB + @Test + public void testTypeAdHoc() { + TypeStruct s1 = TypeStruct.make("s1", new Field[]{ + Field.make("a", TypeInteger.BOT), + Field.make("b", TypeInteger.BOT) }); + TypeStruct s2 = TypeStruct.make("s2", new Field[]{ + Field.make("a", TypeInteger.BOT), + Field.make("b", TypeInteger.BOT) }); + Assert.assertEquals(s1, s1.glb()); + Assert.assertNotEquals(s1, s1.dual()); + Assert.assertEquals(s1, s1.dual().glb()); + + TypeMem m1 = TypeMem.make(1); + TypeMem m2 = TypeMem.make(2); + TypeMem m3 = TypeMem.make(3); + TypeMem m4 = TypeMem.make(4); + + Assert.assertNotEquals(m1, m2); + Assert.assertNotEquals(m2, m3); + Assert.assertNotEquals(m3, m4); + + Assert.assertEquals(TypeStruct.BOT, s1.meet(s2)); + Assert.assertEquals(TypeMem .BOT, m1.meet(m2)); + Assert.assertEquals(TypeMem .BOT, m2.meet(m3)); + Assert.assertEquals(TypeMem .BOT, m3.meet(m4)); + + Assert.assertEquals(TypeMem.BOT, m1.glb()); + Assert.assertEquals(m1, m1.dual()); + Assert.assertEquals(TypeMem.TOP, m1.glb().dual()); + + TypeMemPtr ptr1 = TypeMemPtr.make(s1); + Assert.assertEquals(s1, ptr1._obj); + TypeMemPtr ptr2 = TypeMemPtr.make(s2); + Assert.assertEquals(s2, ptr2._obj); + + TypeMemPtr ptr1nil = TypeMemPtr.make(s1, true); + Assert.assertEquals(s1, ptr1nil._obj); + Assert.assertTrue(ptr1nil._nil); + Assert.assertFalse(ptr1nil._obj==null); + TypeMemPtr ptr2nil = TypeMemPtr.make(s2, true); + Assert.assertEquals(s2, ptr2nil._obj); + + Assert.assertNotEquals(ptr1, ptr2); + Assert.assertNotEquals(ptr1, ptr1.glb()); + Assert.assertEquals(ptr1nil, ptr1.glb()); + + Assert.assertEquals(ptr1, ptr1.dual().dual()); + Assert.assertEquals(ptr1.glb(), ptr1.dual().glb()); + Assert.assertEquals(TypeMemPtr.BOT, ptr1.meet(ptr2nil)); + Assert.assertEquals(ptr1.glb(), ptr1.meet(TypeMemPtr.NULLPTR)); + + TypeMemPtr TOP = TypeMemPtr.TOP; + TypeMemPtr BOT = TypeMemPtr.BOT; + TypeMemPtr PTR = TypeMemPtr.VOIDPTR; + TypeMemPtr NULL = TypeMemPtr.NULLPTR; + Type PTR_meet_NULL = NULL.meet(PTR); + Assert.assertEquals(BOT, PTR_meet_NULL); + Type ptr1_meet_ptr2 = ptr1.meet(ptr2); + Assert.assertEquals(PTR, ptr1_meet_ptr2); + Type NULL_join_ptr1 = NULL.join(ptr1); + Assert.assertEquals(TOP, NULL_join_ptr1); + Type NULL_join_PTR = PTR.join(NULL); + Assert.assertEquals(TOP, NULL_join_PTR); + Type ptr1_dual = ptr1.dual(); + Type nullableptr1_dual = ptr1nil.dual(); + } + + // Test theoretical properties. + // This is a symmetric complete bounded (ranked) lattice. + // Also the meet is commutative and associative. + // The lattice has a dual (symmetric), and join is ~(~x meet ~y). + // See https://en.wikipedia.org/wiki/Lattice_(order). + @Test + public void testLatticeTheory() { + Type[] ts = Type.gather(); + + // Confirm commutative & complete + for( Type t0 : ts ) + for( Type t1 : ts ) { + check_commute (t0,t1); + check_symmetric(t0,t1); + } + + // Confirm associative + for( Type t0 : ts ) + for( Type t1 : ts ) + for( Type t2 : ts ) + assoc(t0,t1,t2); + + // Confirm symmetry. If A isa B, then A.join(C) isa B.join(C) + for( Type t0 : ts ) + for( Type t1 : ts ) + if( t0.isa(t1) ) + for( Type t2 : ts ) { + Type t02 = t0.join(t2); + Type t12 = t1.join(t2); + Type mt = t02.meet(t12); + assertSame(mt,t12); + } + } + + // By design in meet, args are already flipped to order _type, which forces + // symmetry for things with badly ordered _type fields. The question is + // still interesting for other orders. + private static void check_commute( Type t0, Type t1 ) { + if( t0==t1 ) return; + if( t0.is_simple() && !t1.is_simple() ) return; // By design, flipped the only allowed order + Type mta = t0.meet(t1); + Type mtb = t1.meet(t0); // Reverse args and try again + assertSame(mta,mtb); + } + + // A & B = MT + // Expect: ~A & ~MT == ~A + // Expect: ~B & ~MT == ~B + private static void check_symmetric( Type t0, Type t1 ) { + if( t1==t0 ) return; + Type mt = t0.meet(t1); + Type ta = mt.dual().meet(t1.dual()); + Type tb = mt.dual().meet(t0.dual()); + assertSame(ta,t1.dual()); + assertSame(tb,t0.dual()); + } + + private static void assoc( Type t0, Type t1, Type t2 ) { + Type t01 = t0 .meet(t1 ); + Type t12 = t1 .meet(t2 ); + Type t01_2 = t01.meet(t2 ); + Type t0_12 = t0 .meet(t12); + assertSame(t01_2,t0_12); + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java b/chapter10b/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java new file mode 100644 index 00000000..e49e0303 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java @@ -0,0 +1,210 @@ +package com.seaofnodes.simple.evaluator; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Field; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.type.TypeStruct; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Objects; + +public class Evaluator { + + private static final Object MEMORY = new Object(); + + public record Obj(TypeStruct struct, Object[] fields) { + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("Obj<").append(struct._name).append("> {"); + for(int i=0; i= 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 MemMergeNode merge -> MEMORY; + case NewNode alloc -> alloc(alloc); + case ProjNode proj -> ((Object[])val(proj.ctrl()))[proj._idx]; + default -> throw new AssertionError("Unexpected node " + node); + }; + } + + /** + * Run the graph until either a return is found or the number of loop iterations are done. + */ + public Object evaluate(long parameter, int loops) { + if (start == null) return Status.TIMEOUT; + var s = new Object[start.compute()._types.length]; + s[1] = MEMORY; + s[2] = parameter; + values[start._nid] = s; + int i=0; + Scheduler.Block block = this.startBlock; + for(;;) { + for(; i 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) { + // Aggregates describe partitions; they do not overwrite + // memory. Only users of this alias constrain the load. + if (out instanceof MemMergeNode || out instanceof ReturnNode) continue; + if (out instanceof StoreNode s && s._alias != l._alias) continue; + if (out instanceof MemPhiNode p && p._alias != l._alias) continue; + if (out instanceof BulkMemPhiNode p && p.isSplit(l._alias)) continue; + 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 l && l._alias==s._alias) 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/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java new file mode 100644 index 00000000..7dcb88e5 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -0,0 +1,199 @@ +package com.seaofnodes.simple.fuzzer; + +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; + +/** + * Implementation of the fuzzer. + * Random testcases are generated using the ScriptGenerator. + * This will generate a script similar to how the parser parses it but instead of parsing it will + * randomly choose a variation the parser would parse and generate it. + * These scripts are the parsed by the parser and all exceptions are caught and filtered + * to only show one occurrence of one problem. This includes script which generate different + * results when graphs optimized with different worklist seeds are executed. + * To aid debugging scripts that cause errors are then reduced by applying rules and checking + * that the same issue persists. + */ +public class Fuzzer { + + private static final int EVAL_TIMEOUT = 1000; + + /** + * List of exceptions already encountered. + * This is used to filter new exceptions and discard them if they are already found. + */ + private final ArrayList exceptions = new ArrayList<>(); + + /** + * Filter and record an exception in a script. + * @param e The exception caused by the script + * @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, 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 ========="); + System.out.println(Reducer.reduce(script, e, reproducer)); + System.out.println("==========================="); + 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), seed); + } + } + + /** + * Run one test with the given seed, and check for peephole counts. + * @param seed The seed to use for generating this test case + */ + public int fuzzPeepTiming(long seed, int max_nids) { + var rand = new Random(seed); + var sb = new StringBuilder(); + new ScriptGenerator(rand, sb, true).genProgram(); + try { + var parser = new Parser(sb.toString()); + + var stop = parser.parse(); + int parse_peeps= Node.ITER_CNT; + int parse_nops = Node.ITER_NOP_CNT; + double parse_nop_ratio = (double)parse_nops/parse_peeps; + int nids = Node.UID(); + if( nids <= max_nids ) return max_nids; + + double parse_peeps_per_node = (double)parse_peeps/nids; + + stop.iterate(); + int iter_peeps= Node.ITER_CNT; + int iter_nops = Node.ITER_NOP_CNT; + double iter_nop_ratio = (double)iter_nops/iter_peeps; + double iter_peeps_per_node = (double)iter_peeps/nids; + + System.out.printf("%6d | Parsing: peeps: %5d, nops: %5d, ratio: %5.3f, last UID: %5d ps/node: %5.3f | Iter: peeps: %6d, nops: %6d, ratio: %5.3f, ps/node: %5.3f\n", + seed, + parse_peeps,parse_nops,parse_nop_ratio,nids,parse_peeps_per_node, + iter_peeps , iter_nops, iter_nop_ratio, iter_peeps_per_node); + return nids; + } catch (Throwable e) { + // ignore exceptions, let the normal fullPeeps catch and reduce, + // we're interested in valid program optimizations + } + return max_nids; + } + + + /** + * Run one test with the given seed. + * @param seed The seed to use for generating this test case + */ + public void fuzzPeeps(long seed) { + var rand = new Random(seed); + var sb = new StringBuilder(); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); + check(sb.toString(), valid, seed); + } + + + /** + * Check that no exceptions happened. + * @return true if no exception happened + */ + public boolean noExceptions() { + return exceptions.isEmpty(); + } + + /** Run a fixed chapter-local seed without reducing or swallowing failures. */ + public void fuzzPeepsRegression(long seed) { + var rand = new Random(seed); + var sb = new StringBuilder(); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); + try { + runCheck(sb.toString(), valid); + } catch (Throwable e) { + AssertionError ae = new AssertionError("Fuzzer regression seed failed: " + seed); + ae.initCause(e); + throw ae; + } + } +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java new file mode 100644 index 00000000..f947a234 --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -0,0 +1,142 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.util.BitSet; + +/** + * Some utilities for the fuzzer. + */ +class FuzzerUtils { + + /** + * Get the val of a private field + */ + @SuppressWarnings("unchecked") + public static T getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { + return (T) (obj instanceof Class clz ? getField(clz, name).get(null) : getField(obj.getClass(), name).get(obj)); + } + + /** + * Get access to a private field + */ + public static Field getField(Class clz, String name) throws NoSuchFieldException { + var field = clz.getDeclaredField(name); + try { + field.setAccessible(true); + } catch (Throwable e) { + // Intentionally left empty + } + return field; + } + + /** + * Try to rethrow an exception or create a runtime exception and throw it. + */ + public static RuntimeException rethrow(Throwable e) { + if (e instanceof RuntimeException re) return re; + if (e instanceof Error er) throw er; + return new RuntimeException(e); + } + + /** + * Copy of Node.WVISIST + * Used to clear as exceptions might happen in the walk and left this bitset not cleared + */ + private static final BitSet NodeWalkVisit; + private static final java.util.Random WorkRandom; + + /** + * Write access to Iterate.MID_ASSERT + * Exception could happen mid assert and left this at true. Used to reset to false prior to parsing. + */ + private static final MethodHandle set_MID_ASSERT; + + /** + * Some problems might trigger debug output. Suppress this output with a null print stream during fuzzing. + */ + public static final PrintStream NULL_PRINT_STREAM = new PrintStream(OutputStream.nullOutputStream()); + + static { + try { + NodeWalkVisit = getFieldValue(Node.class, "WVISIT"); + WorkRandom = getFieldValue(getFieldValue(IterPeeps.class, "WORK"), "_R"); + set_MID_ASSERT = MethodHandles.lookup().unreflectSetter(getField( IterPeeps.class, "MID_ASSERT")); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Check that the messages are similar. + */ + 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); + } + + /** + * Check if both stack traces have the same first entry. Empty stack traces can happen with NPE optimization + * and allow the other to be any stack trace. + */ + private static boolean sameStackTraceOrigin(StackTraceElement[] a, StackTraceElement[] b) { + if (a == null || a.length == 0 || b == null || b.length == 0) return true; // One or both stack traces are missing. + return a[0].equals(b[0]); + } + + /** + * Checks if two exceptions are caused by the same problem. + */ + 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 (!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 + if (!isExceptionFromSameCause(a.getCause(), b.getCause())) return false; // Different causes + var o1 = a.getSuppressed(); + var o2 = b.getSuppressed(); + if (o1 == null || o2 == null || o1.length != o2.length) return o1 == o2; // Different suppression + for (int i=0; i {} + + /** + * Holder for the rewrite rules + */ + 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("-"), ""), + new Replace(Pattern.compile("\\b\\+-"), "-"), + new Replace(Pattern.compile("(?=])(?:-|\\*|/|>=|<=|>|<|!=|==)(?![+\\-*/<>=])"), "+"), + 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"), + 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+ \\{[^{}]*}"), "") + }; + + /** + * Predicate to test if the reduced script generates the same error. + */ + private final Predicate tester; + + /** + * Create a reducer with a test function. + * @param tester Test function which returns true when the same issue is found in the script. + */ + private Reducer(Predicate tester) { + this.tester = tester; + } + + /** + * Rewrites the script with one rewrite rule. + */ + private String doReplacement(String script, Pattern pattern, Replacer[] replacements) { + var matcher = pattern.matcher(script); + var sb = new StringBuilder(); + 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(); + 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); + } + last = matcher.start(); + skip = 1; + } + sb.append(script, last, script.length()); + return sb.toString(); + } + + /** + * Clean up variable names from the random ones generated by the fuzzer. + */ + private String cleanVariables(String script) { + var matcher = Pattern.compile("\\b([0-9a-zA-Z_]+) ([0-9a-zA-Z_]+)").matcher(script); + int[] num = {0, 0}; + while (matcher.find()) { + 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; + } + } + return script; + } + + /** + * Run all rewrite rules once. + */ + private String doAllReplacements(String script) { + for (var replace: REPLACEMENTS) { + String old; + do { + old = script; + script = doReplacement(script, replace.pattern, replace.replacements); + } while (replace.iterate && !old.equals(script)); + } + return script; + } + + /** + * Reduce the script to a minimal version which produces the same problem with a test function. + * @param tester Test function which returns true when the same issue is found in the script. + */ + public static String reduce(String script, Predicate tester) { + var reducer = new Reducer(tester); + String old; + do { + old = script; + script = reducer.doAllReplacements(script); + script = reducer.cleanVariables(script); + } while (!script.equals(old)); + return script; + } + + /** + * Reduce the script to a minimal version which produces the same problem with a reproducer. + * @param ex The exception originally caused by the script + * @param reproducer Test function which throws an exception which will be checked against ex. + */ + public static String reduce(String script, Throwable ex, Consumer reproducer) { + return reduce(script, s->{ + Throwable nex = null; + try { + reproducer.accept(s); + } catch (Throwable e) { + nex = e; + } + return FuzzerUtils.isExceptionFromSameCause(nex, ex); + }); + } + +} diff --git a/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java new file mode 100644 index 00000000..155063ab --- /dev/null +++ b/chapter10b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -0,0 +1,792 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.Parser; +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. + * This generator will generate the same script for two random number generators initialized with the same seed. + * To generate valid code it implements the parser but instead of parsing it emits the statements and expressions. + * It is guaranteed to terminate. + */ +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 + */ + private static final int INDENTATION = 4; + /** + * Valid characters for identifiers. The last 10 are not valid for the first character. + */ + private static final String VAR_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + /** + * List of keywords not valid for identifiers. + */ + private static final HashSet KEYWORDS; + + static { + try { + KEYWORDS = FuzzerUtils.getFieldValue(new Parser(""), "KEYWORDS"); + } catch (Throwable e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Flag for a statement that it does not pass on control flow to the next statement. + */ + private static final int FLAG_STOP = 0x01; + /** + * Flag for a statement that it contains a final if statement without an else branch. + */ + private static final int FLAG_IF_WITHOUT_ELSE = 0x02; + /** + * Binary operators. + */ + private static final String[] BINARY_OP = {"==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/"}; + /** + * Unary operators. + */ + 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. + */ + private final Random random; + /** + * Indentation depth. + */ + private int indentation = 0; + /** + * Output string builder + */ + private final StringBuilder sb; + /** + * Number of nested loops + */ + private int loopDepth = 0; + /** + * Current scope start in the variables list. + */ + private int currScopeStart = 0; + /** + * The depth of blocks allowed. + */ + private int depth = MAX_BLOCK_DEPTH; + /** + * The depth of expressions allowed. + */ + private int exprDepth = MAX_EXPRESSION_DEPTH; + /** + * Current variables in scope. + */ + 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 boolean generateValid; + /** + * If this script might be invalid. + */ + private boolean maybeInvalid = false; + + + /** + * + * @param random the random number generator. + * @param sb the script will be written to this string builder. + * @param generateValid if the script should be guaranteed valid or if it is allowed to contain bugs. + */ + public ScriptGenerator(Random random, StringBuilder sb, boolean generateValid) { + this.random = random; + this.sb = sb; + this.generateValid = generateValid; + } + + /** + * Random number with log distribution + * @param max The maximum value + * @return A random number in the range [0, max) with higher numbers being more uncommon. + */ + private int randLog(int max) { + max--; + int n = 1<=0; i--) { + if ((num & (1<2) return false; + maybeInvalid = true; + return true; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return The index of the variable in variables or -1 + */ + private int lookup(String name) { + for (int i=variables.size()-1; i>=0; i--) { + if (variables.get(i).name.equals(name)) return i; + } + return -1; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return THe variable of null if not found + */ + private Variable lookupVar(String name) { + int idx = lookup(name); + return idx == -1 ? null : variables.get(idx); + } + + /** + * Add a variable to the current scope + * @param name The name of the variable + * @param type The declared type of the variable + */ + private void addVariable(String name, Type type) { + var old = lookupVar(name); + variables.add(new Variable(name, type, old)); + if (old != null) old.shadowed = true; + } + + /** + * Pop variables to level to + * @param to Level until which variables should be popped + */ + private void popVariables(int to) { + while (variables.size() > to) { + var v = variables.removeLast(); + if (v.shadowing != null) v.shadowing.shadowed = false; + } + } + + /** + * Get a random visible variable patching pred + * @param pred The predicate the variable needs to match + * @return A random visible variable matching pred or null if no variable mached pred. + */ + private Variable findVisibleVariablesMatching(Predicate pred) { + int num=0; + for (var v:variables) { + if (!v.shadowed && pred.test(v)) num++; + } + if (num == 0) return null; + num = random.nextInt(num); + for (var v:variables) { + if (!v.shadowed && pred.test(v) && num-- == 0) return v; + } + throw new AssertionError(); + } + + /** + * Get a structure by name + * @param name Name of the structure + * @return The structure of null + */ + private TypeStruct getStruct(String name) { + for (var s:structs) { + if (s.name.equals(name)) return s; + } + return null; + } + + /** + * Generate a variable name. This might be new random name or a name from an outer scope. + * If the program is allowed to be invalid this can return keywords and already used variable names in the current scope. + * @return The variable name. + */ + private String getVarName() { + if (currScopeStart > 0 && random.nextInt(10) > 7) { + // Use a variable outside the current scope. + var v = variables.get(random.nextInt(currScopeStart)); + if (lookup(v.name) < currScopeStart) return v.name; + if (!generateValid) { + maybeInvalid = true; + return v.name; + } + } + // Generate a new random variable name + StringBuilder sb = getRandomName(); + var v = sb.toString(); + if (!generateValid && (KEYWORDS.contains(v) || lookup(v) >= currScopeStart)) { + maybeInvalid = true; + return v; + } + 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; + addVariable(ScopeNode.ARG0, TYPE_INT); + currScopeStart = variables.size(); + if ((genStatements() & FLAG_STOP) == 0) { + if (generateValid || random.nextInt(10)<7) { + genReturn(); + } else { + this.maybeInvalid = true; + } + } + generateValid = oldGenerateValid; + return !this.maybeInvalid; + } + + /** + * Generate a list of statements. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the last statement generated + */ + public int genStatements() { + 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 -> 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; + var ret = genBlock(); + indentation += INDENTATION; + depth--; + yield ret; + } + case 6 -> genIf(); + case 7 -> genCountedLoop(); + case 8 -> genWhile(); + case 9 -> genAssignment(); + case 10 -> genNullCheck(); + default -> genExit(); + }; + depth++; + } + indentation -= INDENTATION; + allowStructs = oldAllowStructs; + return res; + } + + /** + * Generate an exit. This can be return or break and continue when in a loop. + * @return FLAG_STOP + */ + public int genExit() { + if (loopDepth == 0 && generateValid) return genReturn(); + return switch (random.nextInt(7)) { + case 0,1,2 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("continue;"); + yield FLAG_STOP; + } + case 3,4,5 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("break;"); + yield FLAG_STOP; + } + default -> genReturn(); + }; + } + + /** + * Generate a block statement. After a certain depth only empty blocks are generated to ensure termination of the generator. + * @return flag FLAG_STOP for the generated statement + */ + public int genBlock() { + if (depth == 0) { + sb.append("{}"); + return 0; + } + var oldAllowStructs = allowStructs; + var oldCSS = currScopeStart; + allowStructs = false; + currScopeStart = variables.size(); + depth--; + sb.append("{\n"); + indentation += INDENTATION; + var stop = genStatements(); + indentation -= INDENTATION; + printIndentation().append("}"); + popVariables(currScopeStart); + depth++; + currScopeStart = oldCSS; + allowStructs = oldAllowStructs; + return stop & ~FLAG_IF_WITHOUT_ELSE; + } + + /** + * Generate an if statement with an optional else block. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement + */ + public int genIf() { + sb.append("if("); + genExpression(TYPE_INT); + sb.append(") "); + var stop = genStatementBlock(); + if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { + sb.append("\n"); + printIndentation().append("else "); + stop &= genStatementBlock(); + } else { + stop = FLAG_IF_WITHOUT_ELSE; + } + 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(TYPE_INT); + sb.append(") "); + loopDepth++; + genStatementBlock(); + loopDepth--; + 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() { + 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(";"); + addVariable(name, type); + return 0; + } + + /** + * Generate an assignment statement. + * @return 0 + */ + public int genAssignment() { + 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(declared); + sb.append(";"); + return 0; + } + + /** + * Generate a return statement. + * @return FLAG_STOP + */ + public int genReturn() { + var type = getType(); + sb.append("return "); + genExpression(type); + sb.append(";"); + return FLAG_STOP; + } + + /** + * Generate a binary expression. + * This method does not care about operator precedence. + */ + 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(TYPE_INT); + sb.append(BINARY_OP[random.nextInt(BINARY_OP.length)]); + } + genUnary(TYPE_INT); + } + + /** + * Generate a unary expression. + */ + 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)]); + } + 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(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 constant. + */ + 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(Type type) { + if (generateInvalid()) { + sb.append(getRandomName()); + return; + } + 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); + } + +} diff --git a/chapter11/README.md b/chapter11/README.md index 2a1c3903..c2f329bc 100644 --- a/chapter11/README.md +++ b/chapter11/README.md @@ -1,5 +1,11 @@ # Chapter 11: Global Code Motion +The preceding memory lesson is now split into [10a](../chapter10a/README.md) +and [10b](../chapter10b/README.md). This snapshot still constructs the older +per-alias memory chains in the parser. The language is unchanged; forwarding +10b's bulk-memory representation through Chapters 11-24 is a separate migration. + + # Table of Contents 1. [High Level Overview](#high-level-overview) @@ -16,7 +22,7 @@ 12. [Video Walk Through](#video-walk-through) -You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linear-chapter11) 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-chapter10...linear-chapter11) it to the previous chapter. +You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linear-chapter11) 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-chapter10b...linear-chapter11) it to the previous chapter. The original input source program defines a sequence in which things happen. As we parse the program into Sea of Nodes representation diff --git a/docs/chapter-backports.md b/docs/chapter-backports.md index c2a4b56c..f76f92de 100644 --- a/docs/chapter-backports.md +++ b/docs/chapter-backports.md @@ -7,9 +7,9 @@ are disposable build artifacts. Reusable rules live in Introduce independent fixes in the earliest applicable chapter and propagate them through every affected snapshot. Testing Chapter 25's inherited tests does -not validate the compilers in the earlier chapter directories. Keep building -SSA with incomplete types in Chapter 25 for now; moving that architecture or -renumbering chapters is deferred. Cliff wants to revisit splitting Chapter 25 +not validate the compilers in the earlier chapter directories. Chapter 10 is +now split into 10a (bulk memory) and 10b (lazy memory partitioning). Keep the +rest of SSA construction with incomplete types in Chapter 25 for now. Cliff wants to revisit splitting Chapter 25 into smaller chapters; escape analysis remains in 25 until that larger review. Reset checkpoint (2026-09-28): Cliff committed the completed FunPtr work as @@ -22,6 +22,13 @@ Splitting Chapter 25 is deferred, not an instruction to renumber. ## Pending corrections +- **Forward Chapter 10b memory partitioning through 11-24.** The 10a/10b split + introduces whole-memory SSA first, then `MemMerge`, `MemPhi`, and `BulkMemPhi`. + Later snapshots retain their original memory representation. Migration + boundaries include scheduling in 11, array/allocation memory in 15, the + parser's `ScopeMinNode` in 16, and call/return aggregation in 18. Keep private + constructor memory, escape tracking, and incomplete-type inference in 25. + - **Chapter 24 Load BOTTOM-on-error backport, unwound/deferred (2026-09-28).** Cliff requested starting with 24 and analyzing failures before proceeding. Literal global BOTTOM memory/pointer inputs already propagate BOTTOM. The @@ -210,6 +217,20 @@ The top-level runner accepts explicit chapter lists, e.g. ## Validation record +- **Chapter 10a/10b split (2026-09-29).** Replaced `chapter10` with two + independently buildable snapshots: whole-memory SSA in 10a, then lazy + `MemMerge`/`MemPhi`/`BulkMemPhi` partitioning in 10b. Updated Make discovery, + Maven modules, IDEA descriptors/local module list, chapter navigation, + linear-history link handling, and regenerated the memory diagrams. + Fresh Make builds pass **156 tests in 10a and 159 in 10b**, including the + chapter-local fuzzer regression wrapper. Both release jars and tags build. + Offline Maven tests pass in both modules. Five memory programs each pass + seven inputs across 100 optimizer seeds in each compiler (**3,500 executions + per chapter**). Seed 97's missed bulk-Phi revisit is retained as a regression. + Logs: `build/chapter10-clean-build.log`, `build/chapter10-maven.log`, + `build/10a-seeds.log`, and `build/10b-seeds.log`. Chapters 11-24 have not yet + adopted the new memory representation; that forward migration is queued above. + This is a condensed completion record, not a list of current suite counts for old revisions. Earlier detailed traces are in Git history; durable invariants have been consolidated into the [AI notes](../skills/chapter25-codex-notes.md). diff --git a/graph/README.md b/graph/README.md index cb9ae1f1..812a7318 100644 --- a/graph/README.md +++ b/graph/README.md @@ -40,7 +40,7 @@ Frame numbers start at 1. Jumps lay out only their destination. When the slider moves during layout, the viewer finishes that calculation and then handles the latest request, skipping intermediate requests. Already visited layouts stay cached. -Chapters 10–25 run their existing TypeCheck pass after optimization. A successful +Chapters 10a, 10b, and 11–25 run their existing TypeCheck pass after optimization. A successful check adds a TypeCheck phase-complete frame. An error adds a final graph frame with the compiler's diagnostic; stepping or jumping to it centers the error node at a readable scale, outlines it in red, and shows the message on the phase line diff --git a/graph/graph.iml b/graph/graph.iml new file mode 100644 index 00000000..8e9a0e43 --- /dev/null +++ b/graph/graph.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 19ef8b74..951411f6 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,8 @@ chapter07 chapter08 chapter09 - chapter10 + chapter10a + chapter10b chapter11 chapter12 chapter13 diff --git a/skills/chapter25-codex-notes.md b/skills/chapter25-codex-notes.md index 5cb7dc53..4faf5bdd 100644 --- a/skills/chapter25-codex-notes.md +++ b/skills/chapter25-codex-notes.md @@ -37,6 +37,10 @@ For shared viewer work, read [the graph viewer notes](graph-viewer-codex-notes.m ## Tutorial backports +- Chapter 10 is split into `chapter10a` (one bulk memory chain) and `chapter10b` + (lazy graph partitioning). Both are standalone snapshots. Chapters 11-24 + still use their prior memory representation; forwarding 10b is pending. + - The preferred direction is: introduce a fix in the earliest applicable chapter, then use the same implementation in later snapshots where practical. Tutorial progression takes priority over importing the fully general Chapter 25 solution. @@ -48,7 +52,7 @@ For shared viewer work, read [the graph viewer notes](graph-viewer-codex-notes.m port it into the same test class in every later affected chapter directory. - Every directory contains its own compiler snapshot. Testing Chapter 25's inherited `Chapter10Test` does not validate the Chapter 10 compiler. Use the - top-level runner, e.g. `make -k tests CHAPTERS="chapter10 chapter11"`, with all + top-level runner, e.g. `make -k tests CHAPTERS="chapter10a chapter10b chapter11"`, with all affected directories explicitly listed. Establish the destination baseline before changing it; older Makefiles may need a forced rebuild after API changes. - Keep unrelated discoveries separate in `docs/chapter-backports.md`. Completed -- cgit v1.2.3