From ac525dc59de1ae195c40f8e21b09485d0e5d9330 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Sat, 24 Oct 2026 09:38:56 -0700 Subject: module split --- Makefile | 2 +- README.ja.md | 1 + README.md | 1 + chapter-construction/README.ja.md | 14 +- chapter-construction/README.md | 63 +- .../main/java/com/seaofnodes/simple/Parser.java | 48 +- .../java/com/seaofnodes/simple/node/ScopeNode.java | 27 +- .../java/com/seaofnodes/simple/type/Field.java | 6 +- .../seaofnodes/simple/FinalConstructionTest.java | 63 + chapter-escape/README.ja.md | 3 +- chapter-escape/README.md | 53 +- .../main/java/com/seaofnodes/simple/Parser.java | 48 +- .../java/com/seaofnodes/simple/node/ScopeNode.java | 27 +- .../java/com/seaofnodes/simple/type/Field.java | 6 +- .../seaofnodes/simple/FinalConstructionTest.java | 63 + chapter-incomplete-types/README.md | 69 +- chapter-interprocedural-sccp/README.ja.md | 6 +- chapter-interprocedural-sccp/README.md | 41 +- chapter-modules/Makefile | 201 ++ chapter-modules/README.ja.md | 91 + chapter-modules/README.md | 271 ++ chapter-modules/pom.xml | 46 + chapter-modules/src/main/c/simple_crt.c | 61 + .../main/java/com/seaofnodes/simple/IterPeeps.java | 349 +++ .../main/java/com/seaofnodes/simple/Parser.java | 3157 ++++++++++++++++++++ .../src/main/java/com/seaofnodes/simple/Var.java | 54 + .../com/seaofnodes/simple/codegen/BuildLRG.java | 101 + .../simple/codegen/ClassInitDependencies.java | 214 ++ .../com/seaofnodes/simple/codegen/Coalesce.java | 105 + .../com/seaofnodes/simple/codegen/CodeGen.java | 625 ++++ .../com/seaofnodes/simple/codegen/CompUnit.java | 98 + .../com/seaofnodes/simple/codegen/ElfWriter.java | 460 +++ .../com/seaofnodes/simple/codegen/Encoding.java | 570 ++++ .../com/seaofnodes/simple/codegen/FinalizeIR.java | 44 + .../com/seaofnodes/simple/codegen/FinishCalls.java | 57 + .../simple/codegen/GlobalCodeMotion.java | 304 ++ .../java/com/seaofnodes/simple/codegen/IFG.java | 580 ++++ .../com/seaofnodes/simple/codegen/InstSelect.java | 126 + .../java/com/seaofnodes/simple/codegen/Iter.java | 38 + .../java/com/seaofnodes/simple/codegen/LRG.java | 240 ++ .../com/seaofnodes/simple/codegen/LinkMem.java | 33 + .../seaofnodes/simple/codegen/ListScheduler.java | 261 ++ .../com/seaofnodes/simple/codegen/LocalIds.java | 33 + .../com/seaofnodes/simple/codegen/Machine.java | 127 + .../java/com/seaofnodes/simple/codegen/Opto.java | 317 ++ .../com/seaofnodes/simple/codegen/ParseAll.java | 285 ++ .../com/seaofnodes/simple/codegen/RIPRelSize.java | 14 + .../com/seaofnodes/simple/codegen/RegAlloc.java | 780 +++++ .../com/seaofnodes/simple/codegen/RegMask.java | 121 + .../com/seaofnodes/simple/codegen/RegMaskRW.java | 36 + .../com/seaofnodes/simple/codegen/SourceError.java | 49 + .../com/seaofnodes/simple/codegen/StaticData.java | 177 ++ .../com/seaofnodes/simple/codegen/TypeCheck.java | 57 + .../java/com/seaofnodes/simple/node/AddNode.java | 179 ++ .../java/com/seaofnodes/simple/node/AndNode.java | 49 + .../java/com/seaofnodes/simple/node/ArithNode.java | 208 ++ .../java/com/seaofnodes/simple/node/BoolNode.java | 188 ++ .../com/seaofnodes/simple/node/BulkMemPhiNode.java | 265 ++ .../java/com/seaofnodes/simple/node/CFGNode.java | 303 ++ .../com/seaofnodes/simple/node/CLInitNode.java | 165 + .../java/com/seaofnodes/simple/node/CProjNode.java | 73 + .../com/seaofnodes/simple/node/CallEndMach.java | 38 + .../com/seaofnodes/simple/node/CallEndNode.java | 321 ++ .../java/com/seaofnodes/simple/node/CallNode.java | 220 ++ .../com/seaofnodes/simple/node/CalleeSaveNode.java | 23 + .../com/seaofnodes/simple/node/ConFldOffNode.java | 78 + .../com/seaofnodes/simple/node/ConstantNode.java | 86 + .../com/seaofnodes/simple/node/ConvertNode.java | 101 + .../java/com/seaofnodes/simple/node/CtrlNode.java | 17 + .../java/com/seaofnodes/simple/node/DivNode.java | 40 + .../com/seaofnodes/simple/node/EscapeNode.java | 109 + .../com/seaofnodes/simple/node/ExternNode.java | 27 + .../seaofnodes/simple/node/ExternOffsetNode.java | 16 + .../java/com/seaofnodes/simple/node/FRefNode.java | 50 + .../com/seaofnodes/simple/node/FldOffNode.java | 54 + .../java/com/seaofnodes/simple/node/FunNode.java | 463 +++ .../com/seaofnodes/simple/node/FunPtrNode.java | 62 + .../java/com/seaofnodes/simple/node/GuardMach.java | 17 + .../java/com/seaofnodes/simple/node/GuardNode.java | 62 + .../java/com/seaofnodes/simple/node/IfNode.java | 100 + .../java/com/seaofnodes/simple/node/LoadNode.java | 331 ++ .../java/com/seaofnodes/simple/node/LoopNode.java | 106 + .../seaofnodes/simple/node/MachConcreteNode.java | 26 + .../java/com/seaofnodes/simple/node/MachNode.java | 60 + .../com/seaofnodes/simple/node/MemMergeNode.java | 154 + .../java/com/seaofnodes/simple/node/MemOpNode.java | 140 + .../com/seaofnodes/simple/node/MemPhiNode.java | 62 + .../java/com/seaofnodes/simple/node/MinusNode.java | 77 + .../java/com/seaofnodes/simple/node/ModeNode.java | 12 + .../java/com/seaofnodes/simple/node/MulNode.java | 65 + .../java/com/seaofnodes/simple/node/MultiNode.java | 29 + .../java/com/seaofnodes/simple/node/NeverNode.java | 24 + .../java/com/seaofnodes/simple/node/NewNode.java | 98 + .../main/java/com/seaofnodes/simple/node/Node.java | 696 +++++ .../java/com/seaofnodes/simple/node/NotNode.java | 25 + .../java/com/seaofnodes/simple/node/OrNode.java | 44 + .../java/com/seaofnodes/simple/node/ParmNode.java | 95 + .../java/com/seaofnodes/simple/node/PhiNode.java | 275 ++ .../main/java/com/seaofnodes/simple/node/Proj.java | 2 + .../java/com/seaofnodes/simple/node/ProjNode.java | 59 + .../com/seaofnodes/simple/node/PtrToIntMach.java | 17 + .../com/seaofnodes/simple/node/PtrToIntNode.java | 36 + .../com/seaofnodes/simple/node/ReadOnlyMach.java | 19 + .../com/seaofnodes/simple/node/ReadOnlyNode.java | 27 + .../com/seaofnodes/simple/node/RegionNode.java | 172 ++ .../com/seaofnodes/simple/node/ReturnNode.java | 129 + .../com/seaofnodes/simple/node/RoundF32Node.java | 33 + .../java/com/seaofnodes/simple/node/SarNode.java | 37 + .../java/com/seaofnodes/simple/node/ScopeNode.java | 719 +++++ .../java/com/seaofnodes/simple/node/ShlNode.java | 49 + .../java/com/seaofnodes/simple/node/ShrNode.java | 32 + .../java/com/seaofnodes/simple/node/SplitNode.java | 32 + .../com/seaofnodes/simple/node/StartCUNode.java | 22 + .../java/com/seaofnodes/simple/node/StartNode.java | 87 + .../com/seaofnodes/simple/node/StopCUNode.java | 126 + .../java/com/seaofnodes/simple/node/StopNode.java | 94 + .../java/com/seaofnodes/simple/node/StoreNode.java | 326 ++ .../java/com/seaofnodes/simple/node/SubNode.java | 56 + .../com/seaofnodes/simple/node/ToFloatNode.java | 26 + .../com/seaofnodes/simple/node/ToIntegerNode.java | 24 + .../java/com/seaofnodes/simple/node/TypeNode.java | 32 + .../java/com/seaofnodes/simple/node/XCtrlNode.java | 16 + .../java/com/seaofnodes/simple/node/XorNode.java | 43 + .../seaofnodes/simple/node/cpus/arm/AddARM.java | 21 + .../seaofnodes/simple/node/cpus/arm/AddFARM.java | 23 + .../seaofnodes/simple/node/cpus/arm/AddIARM.java | 28 + .../seaofnodes/simple/node/cpus/arm/AndARM.java | 22 + .../seaofnodes/simple/node/cpus/arm/AndIARM.java | 25 + .../seaofnodes/simple/node/cpus/arm/AsrARM.java | 19 + .../seaofnodes/simple/node/cpus/arm/AsrIARM.java | 32 + .../seaofnodes/simple/node/cpus/arm/BranchARM.java | 80 + .../seaofnodes/simple/node/cpus/arm/CallARM.java | 50 + .../seaofnodes/simple/node/cpus/arm/CallRRARM.java | 33 + .../seaofnodes/simple/node/cpus/arm/CmpARM.java | 23 + .../seaofnodes/simple/node/cpus/arm/CmpFARM.java | 22 + .../seaofnodes/simple/node/cpus/arm/CmpIARM.java | 41 + .../seaofnodes/simple/node/cpus/arm/DivARM.java | 20 + .../seaofnodes/simple/node/cpus/arm/DivFARM.java | 20 + .../seaofnodes/simple/node/cpus/arm/ExtendARM.java | 31 + .../seaofnodes/simple/node/cpus/arm/F8ToIARM.java | 21 + .../seaofnodes/simple/node/cpus/arm/FltARM.java | 45 + .../seaofnodes/simple/node/cpus/arm/FunARM.java | 22 + .../seaofnodes/simple/node/cpus/arm/I2F8ARM.java | 26 + .../seaofnodes/simple/node/cpus/arm/IntARM.java | 39 + .../seaofnodes/simple/node/cpus/arm/LoadARM.java | 34 + .../seaofnodes/simple/node/cpus/arm/LslARM.java | 20 + .../seaofnodes/simple/node/cpus/arm/LslIARM.java | 34 + .../seaofnodes/simple/node/cpus/arm/LsrARM.java | 20 + .../seaofnodes/simple/node/cpus/arm/LsrIARM.java | 29 + .../seaofnodes/simple/node/cpus/arm/MemOpARM.java | 81 + .../seaofnodes/simple/node/cpus/arm/MulARM.java | 20 + .../seaofnodes/simple/node/cpus/arm/MulFARM.java | 19 + .../seaofnodes/simple/node/cpus/arm/NegARM.java | 25 + .../seaofnodes/simple/node/cpus/arm/NewARM.java | 39 + .../seaofnodes/simple/node/cpus/arm/NotARM.java | 28 + .../com/seaofnodes/simple/node/cpus/arm/OrARM.java | 21 + .../seaofnodes/simple/node/cpus/arm/OrIARM.java | 25 + .../seaofnodes/simple/node/cpus/arm/ParmARM.java | 17 + .../seaofnodes/simple/node/cpus/arm/ProjARM.java | 15 + .../seaofnodes/simple/node/cpus/arm/RetARM.java | 16 + .../seaofnodes/simple/node/cpus/arm/SetARM.java | 36 + .../seaofnodes/simple/node/cpus/arm/SplitARM.java | 60 + .../simple/node/cpus/arm/SplitFlagsARM.java | 21 + .../simple/node/cpus/arm/SplitGPRARM.java | 11 + .../seaofnodes/simple/node/cpus/arm/StoreARM.java | 38 + .../seaofnodes/simple/node/cpus/arm/SubARM.java | 20 + .../seaofnodes/simple/node/cpus/arm/SubFARM.java | 19 + .../seaofnodes/simple/node/cpus/arm/SubIARM.java | 29 + .../seaofnodes/simple/node/cpus/arm/TFPARM.java | 51 + .../seaofnodes/simple/node/cpus/arm/TMPARM.java | 49 + .../seaofnodes/simple/node/cpus/arm/UJmpARM.java | 48 + .../seaofnodes/simple/node/cpus/arm/XorARM.java | 20 + .../seaofnodes/simple/node/cpus/arm/XorIARM.java | 27 + .../com/seaofnodes/simple/node/cpus/arm/arm.java | 503 ++++ .../seaofnodes/simple/node/cpus/riscv/AUIPC.java | 42 + .../simple/node/cpus/riscv/AddFRISC.java | 18 + .../simple/node/cpus/riscv/AddIRISC.java | 22 + .../seaofnodes/simple/node/cpus/riscv/AddRISC.java | 21 + .../simple/node/cpus/riscv/AndIRISC.java | 13 + .../seaofnodes/simple/node/cpus/riscv/AndRISC.java | 16 + .../simple/node/cpus/riscv/BranchRISC.java | 95 + .../simple/node/cpus/riscv/CallRISC.java | 62 + .../simple/node/cpus/riscv/CallRRISC.java | 32 + .../simple/node/cpus/riscv/DivFRISC.java | 20 + .../seaofnodes/simple/node/cpus/riscv/DivRISC.java | 17 + .../simple/node/cpus/riscv/ExtendRISC.java | 32 + .../simple/node/cpus/riscv/F8ToIRISC.java | 19 + .../seaofnodes/simple/node/cpus/riscv/FltRISC.java | 48 + .../seaofnodes/simple/node/cpus/riscv/FunRISC.java | 23 + .../simple/node/cpus/riscv/I2F8RISC.java | 24 + .../seaofnodes/simple/node/cpus/riscv/ImmRISC.java | 55 + .../simple/node/cpus/riscv/Int8RISC.java | 49 + .../seaofnodes/simple/node/cpus/riscv/IntRISC.java | 31 + .../com/seaofnodes/simple/node/cpus/riscv/LUI.java | 33 + .../simple/node/cpus/riscv/LoadRISC.java | 65 + .../simple/node/cpus/riscv/MemOpRISC.java | 51 + .../simple/node/cpus/riscv/MulFRISC.java | 18 + .../seaofnodes/simple/node/cpus/riscv/MulRISC.java | 17 + .../seaofnodes/simple/node/cpus/riscv/NegRISC.java | 21 + .../seaofnodes/simple/node/cpus/riscv/NewRISC.java | 49 + .../seaofnodes/simple/node/cpus/riscv/NotRISC.java | 22 + .../seaofnodes/simple/node/cpus/riscv/OrIRISC.java | 13 + .../seaofnodes/simple/node/cpus/riscv/OrRISC.java | 16 + .../simple/node/cpus/riscv/ParmRISC.java | 17 + .../simple/node/cpus/riscv/ProjRISC.java | 15 + .../seaofnodes/simple/node/cpus/riscv/RetRISC.java | 22 + .../simple/node/cpus/riscv/SetFRISC.java | 34 + .../simple/node/cpus/riscv/SetIRISC.java | 21 + .../seaofnodes/simple/node/cpus/riscv/SetRISC.java | 20 + .../simple/node/cpus/riscv/SllIRISC.java | 13 + .../seaofnodes/simple/node/cpus/riscv/SllRISC.java | 17 + .../simple/node/cpus/riscv/SplitRISC.java | 61 + .../simple/node/cpus/riscv/SraIRISC.java | 15 + .../seaofnodes/simple/node/cpus/riscv/SraRISC.java | 17 + .../simple/node/cpus/riscv/SrlIRISC.java | 13 + .../seaofnodes/simple/node/cpus/riscv/SrlRISC.java | 17 + .../simple/node/cpus/riscv/StoreRISC.java | 56 + .../simple/node/cpus/riscv/SubFRISC.java | 18 + .../seaofnodes/simple/node/cpus/riscv/SubRISC.java | 16 + .../seaofnodes/simple/node/cpus/riscv/TFPRISC.java | 56 + .../seaofnodes/simple/node/cpus/riscv/TMPRISC.java | 44 + .../simple/node/cpus/riscv/UJmpRISC.java | 49 + .../simple/node/cpus/riscv/XorIRISC.java | 14 + .../seaofnodes/simple/node/cpus/riscv/XorRISC.java | 16 + .../seaofnodes/simple/node/cpus/riscv/riscv.java | 450 +++ .../simple/node/cpus/x86_64_v2/AddFMemX86.java | 33 + .../simple/node/cpus/x86_64_v2/AddFX86.java | 24 + .../simple/node/cpus/x86_64_v2/AddIX86.java | 23 + .../simple/node/cpus/x86_64_v2/AddMemX86.java | 24 + .../simple/node/cpus/x86_64_v2/AddX86.java | 11 + .../simple/node/cpus/x86_64_v2/AndIX86.java | 11 + .../simple/node/cpus/x86_64_v2/AndX86.java | 11 + .../simple/node/cpus/x86_64_v2/CallRX86.java | 30 + .../simple/node/cpus/x86_64_v2/CallX86.java | 55 + .../simple/node/cpus/x86_64_v2/CmpFX86.java | 24 + .../simple/node/cpus/x86_64_v2/CmpIX86.java | 38 + .../simple/node/cpus/x86_64_v2/CmpMemX86.java | 44 + .../simple/node/cpus/x86_64_v2/CmpX86.java | 28 + .../simple/node/cpus/x86_64_v2/DivFX86.java | 24 + .../simple/node/cpus/x86_64_v2/DivX86.java | 25 + .../simple/node/cpus/x86_64_v2/ExtendX86.java | 29 + .../simple/node/cpus/x86_64_v2/F8ToIX86.java | 22 + .../simple/node/cpus/x86_64_v2/FltX86.java | 39 + .../simple/node/cpus/x86_64_v2/FunX86.java | 32 + .../simple/node/cpus/x86_64_v2/I2f8X86.java | 21 + .../simple/node/cpus/x86_64_v2/ImmX86.java | 32 + .../simple/node/cpus/x86_64_v2/IntX86.java | 43 + .../simple/node/cpus/x86_64_v2/JmpX86.java | 65 + .../simple/node/cpus/x86_64_v2/LeaX86.java | 38 + .../simple/node/cpus/x86_64_v2/LoadX86.java | 44 + .../simple/node/cpus/x86_64_v2/MemAddX86.java | 28 + .../simple/node/cpus/x86_64_v2/MemOpX86.java | 89 + .../simple/node/cpus/x86_64_v2/MulFX86.java | 24 + .../simple/node/cpus/x86_64_v2/MulIX86.java | 27 + .../simple/node/cpus/x86_64_v2/MulX86.java | 24 + .../simple/node/cpus/x86_64_v2/NegX86.java | 20 + .../simple/node/cpus/x86_64_v2/NewX86.java | 24 + .../simple/node/cpus/x86_64_v2/NotX86.java | 21 + .../simple/node/cpus/x86_64_v2/OrIX86.java | 11 + .../simple/node/cpus/x86_64_v2/OrX86.java | 11 + .../simple/node/cpus/x86_64_v2/ParmX86.java | 17 + .../simple/node/cpus/x86_64_v2/ProjX86.java | 15 + .../simple/node/cpus/x86_64_v2/RegX86.java | 22 + .../simple/node/cpus/x86_64_v2/RetX86.java | 15 + .../simple/node/cpus/x86_64_v2/SarIX86.java | 32 + .../simple/node/cpus/x86_64_v2/SarX86.java | 21 + .../simple/node/cpus/x86_64_v2/SetX86.java | 44 + .../simple/node/cpus/x86_64_v2/ShlIX86.java | 32 + .../simple/node/cpus/x86_64_v2/ShlX86.java | 21 + .../simple/node/cpus/x86_64_v2/ShrIX86.java | 27 + .../simple/node/cpus/x86_64_v2/ShrX86.java | 21 + .../simple/node/cpus/x86_64_v2/SplitFlagsX86.java | 25 + .../simple/node/cpus/x86_64_v2/SplitX86.java | 45 + .../simple/node/cpus/x86_64_v2/StoreX86.java | 48 + .../simple/node/cpus/x86_64_v2/SubFX86.java | 23 + .../simple/node/cpus/x86_64_v2/SubX86.java | 10 + .../simple/node/cpus/x86_64_v2/TFPX86.java | 51 + .../simple/node/cpus/x86_64_v2/TMPX86.java | 39 + .../simple/node/cpus/x86_64_v2/UJmpX86.java | 41 + .../simple/node/cpus/x86_64_v2/XorIX86.java | 11 + .../simple/node/cpus/x86_64_v2/XorX86.java | 11 + .../simple/node/cpus/x86_64_v2/x86_64_v2.java | 596 ++++ .../com/seaofnodes/simple/print/ASMPrinter.java | 101 + .../com/seaofnodes/simple/print/IRPrinter.java | 60 + .../simple/print/SimpleGraphAdapter.java | 80 + .../simple/print/SimpleGraphObserver.java | 35 + .../src/main/java/com/seaofnodes/simple/sys.java | 25 + .../java/com/seaofnodes/simple/type/Field.java | 142 + .../main/java/com/seaofnodes/simple/type/Type.java | 548 ++++ .../com/seaofnodes/simple/type/TypeConAry.java | 80 + .../com/seaofnodes/simple/type/TypeConAryB.java | 21 + .../com/seaofnodes/simple/type/TypeConAryI.java | 16 + .../java/com/seaofnodes/simple/type/TypeFloat.java | 111 + .../com/seaofnodes/simple/type/TypeFunPtr.java | 313 ++ .../com/seaofnodes/simple/type/TypeInteger.java | 185 ++ .../java/com/seaofnodes/simple/type/TypeMem.java | 229 ++ .../com/seaofnodes/simple/type/TypeMemPtr.java | 179 ++ .../java/com/seaofnodes/simple/type/TypeNil.java | 61 + .../java/com/seaofnodes/simple/type/TypePtr.java | 57 + .../java/com/seaofnodes/simple/type/TypeRPC.java | 86 + .../com/seaofnodes/simple/type/TypeScalar.java | 45 + .../com/seaofnodes/simple/type/TypeStruct.java | 640 ++++ .../java/com/seaofnodes/simple/type/TypeTuple.java | 158 + .../main/java/com/seaofnodes/simple/type/XInt.java | 244 ++ .../main/java/com/seaofnodes/simple/util/Ary.java | 229 ++ .../java/com/seaofnodes/simple/util/AryInt.java | 192 ++ .../main/java/com/seaofnodes/simple/util/BAOS.java | 162 + .../com/seaofnodes/simple/util/IntHashMap.java | 87 + .../main/java/com/seaofnodes/simple/util/SB.java | 99 + .../java/com/seaofnodes/simple/util/Utils.java | 53 + chapter-modules/src/main/smp/sys.smp | 11 + chapter-modules/src/main/smp/sys/adt.smp | 3 + chapter-modules/src/main/smp/sys/adt/bitset.smp | 57 + chapter-modules/src/main/smp/sys/ary.smp | 24 + chapter-modules/src/main/smp/sys/aryi64.smp | 82 + chapter-modules/src/main/smp/sys/aryu8.smp | 87 + chapter-modules/src/main/smp/sys/char.smp | 8 + chapter-modules/src/main/smp/sys/io.smp | 64 + chapter-modules/src/main/smp/sys/libc.smp | 44 + chapter-modules/src/main/smp/sys/scan.smp | 39 + .../java/com/seaofnodes/simple/BoundaryTest.java | 91 + .../java/com/seaofnodes/simple/BrainFuckTest.java | 87 + .../java/com/seaofnodes/simple/BulkMemPhiTest.java | 40 + .../java/com/seaofnodes/simple/Chapter01Test.java | 95 + .../java/com/seaofnodes/simple/Chapter02Test.java | 61 + .../java/com/seaofnodes/simple/Chapter03Test.java | 66 + .../java/com/seaofnodes/simple/Chapter04Test.java | 159 + .../java/com/seaofnodes/simple/Chapter05Test.java | 204 ++ .../java/com/seaofnodes/simple/Chapter06Test.java | 305 ++ .../java/com/seaofnodes/simple/Chapter07Test.java | 222 ++ .../java/com/seaofnodes/simple/Chapter08Test.java | 208 ++ .../java/com/seaofnodes/simple/Chapter09Test.java | 299 ++ .../java/com/seaofnodes/simple/Chapter10Test.java | 508 ++++ .../java/com/seaofnodes/simple/Chapter12Test.java | 283 ++ .../java/com/seaofnodes/simple/Chapter13Test.java | 284 ++ .../com/seaofnodes/simple/Chapter14FloatTest.java | 104 + .../java/com/seaofnodes/simple/Chapter14Test.java | 228 ++ .../java/com/seaofnodes/simple/Chapter15Test.java | 412 +++ .../java/com/seaofnodes/simple/Chapter16Test.java | 148 + .../java/com/seaofnodes/simple/Chapter17aTest.java | 109 + .../java/com/seaofnodes/simple/Chapter17bTest.java | 621 ++++ .../java/com/seaofnodes/simple/Chapter18Test.java | 522 ++++ .../java/com/seaofnodes/simple/Chapter19Test.java | 459 +++ .../java/com/seaofnodes/simple/Chapter20Test.java | 306 ++ .../java/com/seaofnodes/simple/Chapter21Test.java | 715 +++++ .../java/com/seaofnodes/simple/Chapter22Test.java | 374 +++ .../java/com/seaofnodes/simple/Chapter23Test.java | 341 +++ .../java/com/seaofnodes/simple/Chapter24Test.java | 293 ++ .../simple/ClassInitDependenciesTest.java | 435 +++ .../seaofnodes/simple/CompoundAssignmentTest.java | 94 + .../src/test/java/com/seaofnodes/simple/Eval2.java | 540 ++++ .../seaofnodes/simple/FinalConstructionTest.java | 65 + .../java/com/seaofnodes/simple/FuzzerWrap.java | 78 + .../src/test/java/com/seaofnodes/simple/Main.java | 18 + .../com/seaofnodes/simple/MemoryEncodingTest.java | 104 + .../java/com/seaofnodes/simple/MergeSortTest.java | 83 + .../java/com/seaofnodes/simple/ModulesTest.java | 248 ++ .../java/com/seaofnodes/simple/MutabilityTest.java | 67 + .../com/seaofnodes/simple/NativeExecutionTest.java | 68 + .../test/java/com/seaofnodes/simple/PhaseTest.java | 43 + .../test/java/com/seaofnodes/simple/Simple.java | 254 ++ .../java/com/seaofnodes/simple/SpillStats.java | 92 + .../java/com/seaofnodes/simple/StaticDataTest.java | 172 ++ .../test/java/com/seaofnodes/simple/TestArm64.java | 53 + .../src/test/java/com/seaofnodes/simple/TestC.java | 273 ++ .../test/java/com/seaofnodes/simple/TestRisc5.java | 55 + .../java/com/seaofnodes/simple/TestRunner.java | 17 + .../test/java/com/seaofnodes/simple/TypeTest.java | 223 ++ .../simple/codegen/FlagSpillTestSupport.java | 141 + .../simple/codegen/PrintRegTestSupport.java | 76 + .../simple/codegen/RegAllocTestSupport.java | 352 +++ .../java/com/seaofnodes/simple/fuzzer/Fuzzer.java | 192 ++ .../com/seaofnodes/simple/fuzzer/FuzzerUtils.java | 111 + .../java/com/seaofnodes/simple/fuzzer/Reducer.java | 177 ++ .../seaofnodes/simple/fuzzer/ScriptGenerator.java | 933 ++++++ .../java/com/seaofnodes/simple/progs/arg_count.c | 10 + .../java/com/seaofnodes/simple/progs/brain_fuck.c | 18 + .../java/com/seaofnodes/simple/progs/c_returns.c | 28 + .../java/com/seaofnodes/simple/progs/extern_data.c | 22 + .../seaofnodes/simple/progs/extern_data_import.c | 14 + .../test/java/com/seaofnodes/simple/progs/fib.c | 16 + .../com/seaofnodes/simple/progs/floatToInteger.c | 25 + .../test/java/com/seaofnodes/simple/progs/loop2.c | 7 + .../java/com/seaofnodes/simple/progs/newtonFloat.c | 16 + .../test/java/com/seaofnodes/simple/progs/person.c | 34 + .../test/java/com/seaofnodes/simple/progs/sieve.c | 19 + .../test/java/com/seaofnodes/simple/progs/sort.c | 28 + .../com/seaofnodes/simple/progs/stacked_r_10x.c | 13 + .../com/seaofnodes/simple/progs/stacked_r_11x.c | 13 + .../com/seaofnodes/simple/progs/stacked_r_12x.c | 13 + .../com/seaofnodes/simple/progs/stacked_r_13x.c | 13 + .../com/seaofnodes/simple/progs/stacked_r_9x.c | 13 + .../java/com/seaofnodes/simple/progs/stringHash.c | 30 + .../simple/spill/BrainFuckTest-testBrainFuck.smp | 46 + .../simple/spill/Chapter20Test-testAlloc0.smp | 1 + .../simple/spill/Chapter20Test-testAlloc2.smp | 1 + .../simple/spill/Chapter20Test-testArray1.smp | 8 + .../simple/spill/Chapter20Test-testBasic1.smp | 1 + .../simple/spill/Chapter20Test-testBrainfuck.smp | 43 + .../simple/spill/Chapter20Test-testCast.smp | 1 + .../simple/spill/Chapter20Test-testFlags1.smp | 5 + .../simple/spill/Chapter20Test-testFlags2.smp | 6 + .../simple/spill/Chapter20Test-testFltArg.smp | 1 + .../simple/spill/Chapter20Test-testMergeSort.smp | 46 + .../simple/spill/Chapter20Test-testNewtonFloat.smp | 11 + .../spill/Chapter20Test-testNewtonInteger.smp | 10 + .../simple/spill/Chapter20Test-testString.smp | 35 + .../simple/spill/Chapter21AllocTest-testArray1.smp | 9 + .../spill/Chapter21AllocTest-testNewtonFloat.smp | 13 + .../simple/spill/Chapter21AllocTest-testString.smp | 35 + .../simple/spill/Chapter21Test-testAntiDeps1.smp | 10 + .../simple/spill/Chapter21Test-testArgCount-2.smp | 6 + .../simple/spill/Chapter21Test-testArgCount.smp | 6 + .../simple/spill/Chapter21Test-testFibExport.smp | 10 + .../simple/spill/Chapter21Test-testInfinite.smp | 1 + .../simple/spill/Chapter21Test-testLoop2.smp | 9 + .../spill/Chapter21Test-testNewtonExport.smp | 10 + .../simple/spill/Chapter21Test-testPerson.smp | 9 + .../simple/spill/Chapter21Test-testSieve.smp | 30 + .../spill/Chapter21Test-testStringExport.smp | 33 + .../simple/spill/Chapter22Test-testHelloWorld.smp | 4 + .../spill/Chapter22Test-testInfiniteReturn.smp | 1 + .../spill/Chapter22Test-testNarrowCReturns.smp | 12 + .../simple/spill/Chapter22Test-testPerson.smp | 8 + .../simple/spill/Chapter22Test-testSextFail.smp | 4 + .../simple/spill/Chapter22Test-testSextFail2.smp | 5 + .../simple/spill/Chapter22Test-testSextSuccess.smp | 5 + .../simple/spill/Chapter23AllocTest-testString.smp | 34 + .../simple/spill/Chapter23Test-testAnd.smp | 11 + .../simple/spill/Chapter23Test-testAndPtr.smp | 12 + .../spill/Chapter23Test-testCondSideEffAnd.smp | 17 + .../spill/Chapter23Test-testCondSideEffOr.smp | 24 + .../simple/spill/Chapter23Test-testFRefFields.smp | 20 + .../simple/spill/Chapter23Test-testFuncCall.smp | 16 + .../simple/spill/Chapter23Test-testMethod.smp | 17 + .../simple/spill/Chapter23Test-testOr.smp | 13 + .../spill/Chapter23Test-testOrRhsLoopLocal.smp | 10 + .../spill/Chapter23Test-testOrRhsLoopMemory.smp | 11 + .../spill/Chapter24AllocTest-testBubbleSort.smp | 217 ++ .../spill/Chapter24AllocTest-testNewtonInteger.smp | 11 + .../Chapter24AllocTest-testStackArguments.smp | 5 + .../simple/spill/Chapter24Test-testStack1.smp | 10 + .../simple/spill/Chapter24Test-testStack10-2.smp | 1 + .../simple/spill/Chapter24Test-testStack10.smp | 3 + .../simple/spill/Chapter24Test-testStack11-2.smp | 1 + .../simple/spill/Chapter24Test-testStack11.smp | 3 + .../simple/spill/Chapter24Test-testStack12-2.smp | 1 + .../simple/spill/Chapter24Test-testStack12.smp | 3 + .../simple/spill/Chapter24Test-testStack13-2.smp | 1 + .../simple/spill/Chapter24Test-testStack13.smp | 3 + .../simple/spill/Chapter24Test-testStack2.smp | 10 + .../simple/spill/Chapter24Test-testStack3.smp | 11 + .../simple/spill/Chapter24Test-testStack4.smp | 10 + .../simple/spill/Chapter24Test-testStack5.smp | 15 + .../simple/spill/Chapter24Test-testStack9-2.smp | 1 + .../simple/spill/Chapter24Test-testStack9.smp | 3 + .../simple/spill/MergeSortTest-testMergeSort0.smp | 35 + .../java/com/seaofnodes/simple/spill/README.ja.md | 50 + .../java/com/seaofnodes/simple/spill/README.md | 78 + .../java/com/seaofnodes/simple/spill/cohorts.tsv | 200 ++ .../test/java/com/seaofnodes/simple/test0/A.smp | 6 + .../test/java/com/seaofnodes/simple/test0/A/B.smp | 1 + .../test/java/com/seaofnodes/simple/test5/par.smp | 20 + .../com/seaofnodes/simple/test5/par/child1.smp | 12 + .../com/seaofnodes/simple/test5/par/child2.smp | 13 + .../seaofnodes/simple/test_smp/forward_ctor/m.smp | 2 + .../simple/test_smp/forward_ctor/m/a.smp | 1 + .../simple/test_smp/forward_ctor/m/b.smp | 4 + .../seaofnodes/simple/test_smp/seed_repro/sys.smp | 3 + .../simple/test_smp/seed_repro/sys/ary.smp | 6 + .../simple/test_smp/seed_repro/sys/aryi64.smp | 29 + .../simple/test_smp/seed_repro/sys/aryu8.smp | 50 + chapter-modules/src/test/smp/lazy/App.smp | 3 + chapter-modules/src/test/smp/lazy/App/Main.smp | 1 + chapter-modules/src/test/smp/lazy/App/Stats.smp | 1 + chapter-modules/src/test/smp/lazy/App/Tables.smp | 4 + chapter-sccp/README.ja.md | 12 +- chapter-sccp/README.md | 28 +- .../java/com/seaofnodes/simple/Chapter19Test.java | 10 +- chapter25/README.ja.md | 35 +- chapter25/README.md | 13 +- docs/chapter-backports.md | 167 +- docs/chapter-split.md | 31 +- pom.xml | 1 + skills/chapter25-codex-notes.md | 17 +- 485 files changed, 43722 insertions(+), 187 deletions(-) create mode 100644 chapter-construction/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java create mode 100644 chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java create mode 100644 chapter-modules/Makefile create mode 100644 chapter-modules/README.ja.md create mode 100644 chapter-modules/README.md create mode 100644 chapter-modules/pom.xml create mode 100644 chapter-modules/src/main/c/simple_crt.c create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/IterPeeps.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/Parser.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/Var.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Encoding.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/IFG.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Iter.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LRG.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Machine.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Opto.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMask.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMaskRW.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/SourceError.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/StaticData.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/AddNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/AndNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ArithNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/BoolNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/CFGNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/CLInitNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/CProjNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndMach.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/CallNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/CalleeSaveNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ConstantNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ConvertNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/CtrlNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/DivNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/EscapeNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/FRefNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/FldOffNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/FunNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardMach.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/IfNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/LoadNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/LoopNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/MachNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/MemOpNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/MinusNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ModeNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/MulNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/MultiNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/NeverNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/NewNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/Node.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/NotNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/OrNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ParmNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/PhiNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/Proj.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ProjNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntMach.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyMach.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/RegionNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ReturnNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/SarNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ScopeNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ShlNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ShrNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/SplitNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/StartCUNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/StartNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/StopCUNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/StopNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/StoreNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/SubNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/TypeNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/XorNode.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddFARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallRRARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpFARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/DivARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/DivFARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/F8ToIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/FltARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/FunARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/I2F8ARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulFARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NegARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NotARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ParmARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ProjARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/RetARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubFARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/UJmpARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorIARM.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddFRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BranchRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/DivFRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/DivRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/F8ToIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FltRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FunRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/I2F8RISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ImmRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LUI.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulFRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NegRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NotRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ParmRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ProjRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/RetRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetFRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubFRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/UJmpRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorIRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorRISC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpFX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivFX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/F8ToIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/I2f8X86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ImmX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LeaX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RegX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/UJmpX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorX86.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/print/IRPrinter.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/sys.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/Field.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/Type.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAry.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFloat.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeInteger.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMem.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeNil.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypePtr.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeRPC.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeScalar.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeStruct.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeTuple.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/type/XInt.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/util/Ary.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/util/AryInt.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/util/BAOS.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/util/IntHashMap.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/util/SB.java create mode 100644 chapter-modules/src/main/java/com/seaofnodes/simple/util/Utils.java create mode 100644 chapter-modules/src/main/smp/sys.smp create mode 100644 chapter-modules/src/main/smp/sys/adt.smp create mode 100644 chapter-modules/src/main/smp/sys/adt/bitset.smp create mode 100644 chapter-modules/src/main/smp/sys/ary.smp create mode 100644 chapter-modules/src/main/smp/sys/aryi64.smp create mode 100644 chapter-modules/src/main/smp/sys/aryu8.smp create mode 100644 chapter-modules/src/main/smp/sys/char.smp create mode 100644 chapter-modules/src/main/smp/sys/io.smp create mode 100644 chapter-modules/src/main/smp/sys/libc.smp create mode 100644 chapter-modules/src/main/smp/sys/scan.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/BoundaryTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/BrainFuckTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter01Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter02Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter03Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter04Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter05Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter06Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter07Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter08Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter09Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter10Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter12Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter13Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter14Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter15Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter16Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter17aTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter17bTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter18Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter19Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter20Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter21Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter22Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter23Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Chapter24Test.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Eval2.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/FuzzerWrap.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Main.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/MergeSortTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/ModulesTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/MutabilityTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/PhaseTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/Simple.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/SpillStats.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/StaticDataTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/TestArm64.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/TestC.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/TestRisc5.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/TestRunner.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/TypeTest.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/arg_count.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/brain_fuck.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/c_returns.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/extern_data.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/fib.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/floatToInteger.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/loop2.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/newtonFloat.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/person.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/sieve.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/sort.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_10x.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_11x.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_12x.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_13x.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_9x.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/progs/stringHash.c create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/BrainFuckTest-testBrainFuck.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc0.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testArray1.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBasic1.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBrainfuck.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testCast.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags1.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFltArg.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testMergeSort.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonFloat.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonInteger.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testArray1.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testNewtonFloat.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testAntiDeps1.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount-2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testFibExport.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testInfinite.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testLoop2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testNewtonExport.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testPerson.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testSieve.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testHelloWorld.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testInfiniteReturn.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testNarrowCReturns.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testPerson.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextSuccess.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAnd.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAndPtr.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffAnd.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffOr.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFRefFields.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFuncCall.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testMethod.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOr.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopLocal.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopMemory.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testNewtonInteger.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testStackArguments.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack1.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10-2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11-2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12-2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13-2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack3.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack4.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack5.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9-2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/MergeSortTest-testMergeSort0.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/README.ja.md create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/README.md create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/spill/cohorts.tsv create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test0/A.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test0/A/B.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test5/par.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test5/par/child1.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test5/par/child2.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/a.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/b.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/ary.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryi64.smp create mode 100644 chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryu8.smp create mode 100644 chapter-modules/src/test/smp/lazy/App.smp create mode 100644 chapter-modules/src/test/smp/lazy/App/Main.smp create mode 100644 chapter-modules/src/test/smp/lazy/App/Stats.smp create mode 100644 chapter-modules/src/test/smp/lazy/App/Tables.smp diff --git a/Makefile b/Makefile index ccca143a..77651489 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ .DEFAULT_GOAL := tests # Named snapshots follow teaching order, not alphabetical order. Numbered # chapters remain comparison baselines while the split is being extracted. -NAMED_CHAPTERS := chapter-sccp chapter-interprocedural-sccp chapter-incomplete-types chapter-construction chapter-escape +NAMED_CHAPTERS := chapter-sccp chapter-interprocedural-sccp chapter-incomplete-types chapter-construction chapter-escape chapter-modules CHAPTERS ?= $(sort $(patsubst %/pom.xml,%,$(wildcard chapter[0-9][0-9]/pom.xml chapter[0-9][0-9][a-z]/pom.xml))) $(NAMED_CHAPTERS) ACTIONS := tests tags release lib CHAPTER_TARGETS := $(foreach action,$(ACTIONS),$(addsuffix /$(action),$(CHAPTERS))) diff --git a/README.ja.md b/README.ja.md index b544e639..014f0add 100644 --- a/README.ja.md +++ b/README.ja.md @@ -72,6 +72,7 @@ Simple 言語のスタイルは、C または Java のサブセットに倣っ * [型が確定する前の SSA 構築](chapter-incomplete-types/README.ja.md) * [オブジェクトの構築と非公開メモリ](chapter-construction/README.ja.md) * [エスケープ解析と未知の呼び出し元](chapter-escape/README.ja.md) +* [モジュールと遅延クラス初期化](chapter-modules/README.ja.md) ## 章をまたぐビルド diff --git a/README.md b/README.md index ef619dbb..6aba3b05 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ baselines during extraction. The first named snapshots are: * [SSA Before Types Are Known](chapter-incomplete-types/README.md) * [Construction and Private Memory](chapter-construction/README.md) * [Escape Analysis and Unknown Callers](chapter-escape/README.md) +* [Modules and Lazy Class Initialization](chapter-modules/README.md) ## Building across chapters diff --git a/chapter-construction/README.ja.md b/chapter-construction/README.ja.md index 86104de0..2fa83280 100644 --- a/chapter-construction/README.ja.md +++ b/chapter-construction/README.ja.md @@ -11,10 +11,18 @@ フィールドごとの非公開メモリを次の関数に渡します。構築が完了した時点で、 `EscapeNode` が初期化済みのフィールドを公開メモリに合流させます。 -全ての実行経路で必須フィールドが初期化されていることを確認します。 +パーサーはコンストラクタの出口で、必須フィールドが初期化されていることを確認します。 +この検査は SCCP より前なので、後の解析で安全と分かる呼び出しでも拒否する場合があります。 構築中の `self` を通常の関数に渡したり、オブジェクトに格納したりする -ことは、この章では禁止します。別名や呼び出しを通じた公開の追跡は、 -次のエスケープ解析の章で扱います。この境界は引き続き検討します。 +ことは、この章では禁止します。次のエスケープ解析の章でもこの制限は残ります。 +インライン化後に安全性を確認してヘルパー呼び出しを許す案は、まだ実装していません。 + +未完成のオブジェクトをコンストラクタ内で読むことはできますが、読むフィールドは +すでに初期化されている必要があります。他のフィールドはまだ未設定でも構いません。 +final フィールドも構築中はローカル値として再代入でき、ループでは通常の Phi を使います。 +Store はヘルパーの出口で生成します。構築後の書き換え、通常のローカル `val` への再代入、 +共有する関数宣言の置き換えは禁止します。 +詳細と提案中の規則は [英語版](README.md#helper-calls-and-final-fields-during-construction) を参照してください。 モジュール、クラスの遅延初期化、分割コンパイルはまだ導入しません。 詳しい説明、制限、実行例は [英語版](README.md) を参照してください。 diff --git a/chapter-construction/README.md b/chapter-construction/README.md index c61bd7b7..f0ea4222 100644 --- a/chapter-construction/README.md +++ b/chapter-construction/README.md @@ -75,7 +75,7 @@ including their existing integer range widening. For example: ```simple struct Counter { - int x; + val x = 0; new Counter = { int n -> while (x < n) x++; }; }; return new Counter(arg).x; @@ -86,6 +86,17 @@ performs no field Store on each trip. At the constructor exit, the compiler stores the final value into the new object. This is the result scalar replacement would give us, built directly from the construction rules. +Even final data fields can change during construction; they become final when +the constructor finishes. `x = 1; x = 2;` leaves the local value 2 for the exit +Store. In the loop above, `x++` updates a local Phi, not memory. No sequence of +final field Stores needs cleaning up inside the loop. + +A constant default does not make a field shared by every instance: a later +constructor may change it. Inferred field types therefore describe the value's +family, not just its initial constant. Ordinary local `val` bindings remain +final. Shared function declarations are also fixed; a constructor cannot replace +one of those functions. + Private memory is still useful at the helper boundaries. `NewNode` produces the pointer and its initial private memory. The hidden initializer computes defaults as local values and emits ordinary initializing Stores at exit. The @@ -109,18 +120,17 @@ initialized length are published before the array is returned. Required non-null fields have no legal zero default. Reading one before it is written is an error. - ## Publish only after completion `EscapeNode` publishes one field. It consumes control, public memory, private memory, and `self`; its result joins the field's value into the public alias. Publication has no machine instruction, but its memory edges keep initializing -Stores before later Loads and Stores. Every field is published independently, +Stores before later Loads and Stores. Every field is published independently, and scheduling may well interleave the initializing writes from many unrelated allocations and public memory updates. Before publication, every required field must be initialized on every live -path. This is rejected when `arg` can be zero: +path. This constructor is rejected: ```simple struct Box { @@ -130,10 +140,44 @@ struct Box { return new Box(arg); ``` -This is rejected because `int[] data` requires a non-null initial value, and if -`arg` is null, `data` is never set not-null. This check is based on the type -of every field at the constructor exit; if type analysis (e.g. SCCP) determines -the field is set, the code is good; if not then not. +`int[] data` requires a non-null initial value. If `n` is zero, `data` is never +set. The current parser follows explicit missing-initialization values through +the constructor's merges and checks its exit. This happens before SCCP: even +calling this constructor only with `new Box(1)` does not rescue it. Allowing +later type analysis to prove completion would require deferring this check. + + +## Helper calls and *final* fields during construction + +Constructor code may read the unfinished object, but never an uninitialized +field. Ordinary code must not observe it before construction finishes. + +We would like to allow helper calls when optimization proves they do not expose +the unfinished object. Inlining could turn a helper's field accesses into local +values. After optimization reaches a fixed point, each final field would have +a final Store: before that line, accesses use local values; after it, the field +can be read but never written. Mutable fields can still be written later. + +This is a proposed relaxation, not implemented in this snapshot. The parser +currently rejects passing `self` to an ordinary method before inlining can +prove it safe. For example, we would like this constructor to work: + +```simple +struct Vec { + int _max; + u8[~] !_buf; // Not-null, must be set before reading + + new Vec = { -> + _buf = new u8[1]; + clear(); // Would be safe after inlining; currently rejected + }; + val clear = { -> + // Keep at most 1K of storage in a cleared Vec + if( _buf# > 1024 ) _buf = new u8[1024]; + _max=0; + }; +}; +``` ## Build and check @@ -153,3 +197,6 @@ make tests CHAPTERS=chapter-construction CTAGS= defaults, completion errors, forward fields and methods, private loops, native execution, and ARM/RISC-V execution. The inherited language, memory, lattice, and backend suites run alongside it. +`FinalConstructionTest` checks repeated assignments and loops over final fields, +reads through methods, rejection of later writes, and native execution. It also +checks that the loop's field Stores are outside the loop. diff --git a/chapter-construction/src/main/java/com/seaofnodes/simple/Parser.java b/chapter-construction/src/main/java/com/seaofnodes/simple/Parser.java index 5ce7c410..dabfb6a2 100644 --- a/chapter-construction/src/main/java/com/seaofnodes/simple/Parser.java +++ b/chapter-construction/src/main/java/com/seaofnodes/simple/Parser.java @@ -267,7 +267,7 @@ public class Parser { // Pre-call the function from Start, with worse-case arguments. This // represents all the future, yet-to-be-parsed functions calls and // external calls. - _scope.push(new Kind.Func()); + _scope.push(new Kind.Func(_privateFun!=null)); ctrl(fun); // Scope control from function // Unknown callers supply whole memory; field aliases are discovered lazily. _scope.mem(new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,con(TypeMem.BOT)).peephole()); @@ -768,6 +768,7 @@ public class Parser { // The alloc() call added the allocation scope !_scope.canInit(def) ) throw error("Cannot reassign final '"+name+"'"); + checkSharedField(def); // Parse assignment expression checkCaptureWrite(def); @@ -869,6 +870,14 @@ public class Parser { throw error("a not-null/non-zero expression"); t = expr._type; if( !xfinal ) t = inferredType(t); // Widen if not final + // A final field is still a local variable during construction. + // Its constant default is a value, not a one-value declaration + // type: later assignments and loop Phis may change it. + else if( _scope.inConstructor() && !(t instanceof TypeFunPtr) ) { + Type wide=inferredType(t); + t = t instanceof TypeMemPtr ptr && ptr.notNull() + ? TypeMemPtr.make((byte)2,((TypeMemPtr)wide)._obj,false).withAccess(ptr._ro) : wide; + } } // expr is a constant function if( t instanceof TypeFunPtr && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { @@ -948,7 +957,10 @@ public class Parser { Var v = _scope.var(index++); if( v.isFRef() ) continue; Node value = _scope.in(v); - boolean constant = v._final && v._init && value._type.isConstant(); + // Only function declarations are shared across instances. A final + // data field can change in a named or inline constructor, so a + // constant default does not justify folding every future read. + boolean constant = v._final && v._init && value._type instanceof TypeFunPtr && value._type.isConstant(); Field field = Field.make(v._name,v.type(),_code.getALIAS(),v._final,constant); shape=shape.add(field); TYPES.put(_nestedType,TypeMemPtr.make(shape)); @@ -1452,8 +1464,9 @@ public class Parser { if( name!=null ) { Var n = _scope.lookup(name); if( n != null && !(n.type() instanceof TypeMemPtr) ) { - if( n._final ) + if( n._final && !_scope.canInit(n) ) throw error("Cannot reassign final '"+n._name+"'"); + checkSharedField(n); Node expr = liftExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type(),true); _scope.update(n,expr); return expr; @@ -1545,8 +1558,9 @@ public class Parser { if( ch==0 ) // Normal primary, check for postfix updates return parsePostfix(rvalue); // Assign-update direct into Scope - if( var._final ) + if( var._final && !_scope.canInit(var) ) throw error("Cannot reassign final '"+var._name+"'"); + checkSharedField(var); checkCaptureWrite(var); Node op = opAssign(ch,rvalue, var.type() ); _scope.update(var,op); @@ -1600,6 +1614,24 @@ public class Parser { throw error("Cannot assign captured variable '"+var._name+"' in an initializer"); } + // Inline constructors have local field slots too, but shared function + // declarations have no instance slot to publish. Do not silently discard + // an assignment to one of those declarations. + private void checkSharedField(Var var) { + if( !_scope.canInit(var) ) return; + if( _scope.kind(var) instanceof Kind.Define ) { + checkPrivateWrite(var._name); + return; + } + for( int i=_scope.depth()-1; i>0; i-- ) + if( _scope._kinds.at(i) instanceof Kind.Alloc alloc ) { + Field field=alloc._tmp._obj.field(var._name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+var._name+"'"); + return; + } + } + // Check for assign-update, "x += e0". // Returns "x + e0"; the caller assigns value. // if ch is postfix, then lhs is kept and caller will unkeep and return it. @@ -1980,6 +2012,7 @@ public class Parser { if( v.type() instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) throw error("A constructor is only called by new"); if( matchOpx('=','=') ) { + checkPrivateWrite(name); Node value=parseAsgn().keep(); checkPublication(value); _scope.update(v,liftExpr(value,v.type(),false)); @@ -1990,6 +2023,7 @@ public class Parser { throw error("Cannot read uninitialized field '"+name+"'"); char ch=_lexer.matchOperAssign(); if( ch!=0 ) { + checkPrivateWrite(name); Node op=opAssign(ch,value,v.type()); checkPublication(op); _scope.update(v,op); @@ -2000,6 +2034,12 @@ public class Parser { return parsePostfix(value); } + private void checkPrivateWrite(String name) { + Field field=((TypeMemPtr)TYPES.get(_nestedType))._obj.field(name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+name+"'"); + } + // zero/sign extend. "i" is limited to either classic unsigned (min==0) or // classic signed (min=minus-power-of-2); max=power-of-2-minus-1. private Node zsMask(Node val, Type t ) { diff --git a/chapter-construction/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter-construction/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 3c1e9776..f65a822d 100644 --- a/chapter-construction/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/chapter-construction/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -39,7 +39,10 @@ public class ScopeNode extends Node { // Basic block scoping public static class Block extends Kind { } // Function scope - public static class Func extends Kind { } + public static class Func extends Kind { + public final boolean _constructor; + public Func(boolean constructor) { _constructor=constructor; } + } // Struct definition, mapping types to fields public static class Define extends Kind { public Define(TypeMemPtr tmp) { _tmp=tmp; } public final TypeMemPtr _tmp; } // Struct allocation, mapping values to fields @@ -137,15 +140,25 @@ public class ScopeNode extends Node { } - // Allocation fields live in the scope immediately outside the constructor - // block. Nested blocks retain that permission; surrounding bindings do not. + // Fields are writable local values until construction ends, even when the + // published field is final. Ordinary methods and final local variables do + // not inherit that permission. This also decides which loop Phis we need. public boolean canInit(Var v) { - for( int i=_kinds._len-1; i>0; i-- ) - if( _kinds.at(i) instanceof Kind.Alloc ) + for( int i=_kinds._len-1; i>=0; i-- ) { + Kind k=_kinds.at(i); + if( k instanceof Kind.Alloc ) return v._idx >= _kinds.at(i-1)._lexSize && v._idx < _kinds.at(i)._lexSize; + if( k instanceof Kind.Define ) return kind(v)==k; + if( k instanceof Kind.Func fun ) + return fun._constructor && i>0 && _kinds.at(i-1) instanceof Kind.Define def && kind(v)==def; + } return false; } + private boolean loopVar(Var v) { + return !v._final || (canInit(v) && !v.type().isHighOrConst()); + } + public boolean inConstructor() { return _kinds.last() instanceof Kind.Define; } public boolean inAllocation () { return _kinds.last() instanceof Kind.Alloc ; } public boolean inFunction () { return _kinds.last() instanceof Kind.Func ; } @@ -294,7 +307,7 @@ public class ScopeNode extends Node { // Memory is one binding, with the same lazy loop Phi as scalar names. for( int i=1; i FREE = new Ary<>(Field.class); diff --git a/chapter-construction/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter-construction/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java new file mode 100644 index 00000000..e91dc384 --- /dev/null +++ b/chapter-construction/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -0,0 +1,63 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.StoreNode; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FinalConstructionTest { + private static final String LOOP = """ + val step={int x -> x*2+1;}; + struct C { + val x=1; + new C={int n -> + x=0; x=1; + for(int i=0; ix;}; + }; + return new C(arg).get(); + """; + + @Test public void testFinalConstructorLocals() throws java.io.IOException { + var code=new CodeGen(LOOP).driver(CodeGen.Phase.LocalSched); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("15",Eval2.eval(code,3)); + assertEquals("63",Eval2.eval(code,5)); + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("x") ) + assertEquals("Final field writes belong after the loop",0,st.cfg0().loopDepth()); + return null; + }); + String[] sources={ + "struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;", + "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i for(int i=0;ix=2.5;}; }; return new C().x;", + "struct C { val x=\"a\"; new C={int n -> for(int i=0;ix=new int[2]; x=new int[3];}; }; return new C().x#;" + }; + String[] results={"2","8","4","2.5","98","3"}; + for( int i=0; ival x=1; x=2;}; }; return new C();", + "struct C { val f={->1;}; new C={->f={->2;};}; }; return new C().f();" + }; + for( String src : rejected ) { + try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(src); } + catch( Parser.ParseException e ) { assertTrue(e.getMessage(),e.getMessage().startsWith("Cannot")); } + } + + var dir=java.nio.file.Path.of("build/objs"); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.smp"),"val buildFinal={int arg ->"+LOOP+"};"); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.c"),""" + extern long long buildFinal(long long); + int main(void) { return buildFinal(0)!=1 || buildFinal(3)!=15 || buildFinal(5)!=63; } + """); + TestC.run("build/objs","finalConstruction","",-1); + } +} diff --git a/chapter-escape/README.ja.md b/chapter-escape/README.ja.md index 43bb4411..c496df4c 100644 --- a/chapter-escape/README.ja.md +++ b/chapter-escape/README.ja.md @@ -2,7 +2,8 @@ [English](README.md) | 日本語 -[前章: オブジェクトの構築と非公開メモリ](../chapter-construction/README.ja.md) +[前章: オブジェクトの構築と非公開メモリ](../chapter-construction/README.ja.md) | +[次: モジュールと遅延クラス初期化](../chapter-modules/README.ja.md) この章では、オブジェクトや関数のアドレスが、解析できない外部コードに届くかを追跡します。 関数のアドレスを既知の関数に渡すだけでは、未知の呼び出し元を残す必要はありません。 diff --git a/chapter-escape/README.md b/chapter-escape/README.md index 6098c72c..a1fda17b 100644 --- a/chapter-escape/README.md +++ b/chapter-escape/README.md @@ -2,14 +2,15 @@ English | [日本語](README.ja.md) -[Previous: Construction and Private Memory](../chapter-construction/README.md) +[Previous: Construction and Private Memory](../chapter-construction/README.md) | +[Next: Modules and Lazy Class Initialization](../chapter-modules/README.md) We run a simple form of escape analysis in this chapter, tracking both functions and some aliases across all calls and memory updates. One of the goals here is to discover when we've found *all* the callers of a function; at this point we can drop the conservative "unknown caller" input and run -constant propagation through a functions arguments. +constant propagation through a function's arguments. The preceding compiler kept an unknown caller for every surviving function address. That was safe, but it lost facts even when all uses stayed within the @@ -44,7 +45,7 @@ SCCP can then infer `base == 2`, including in its recursive body. The multiply by 2 can become a shift without inlining or cloning either recursive body - and we open ourselves up to further optimizations. -Passing `power` to an unknown caller, or passing it to a native function, +Passing `power` to an unknown callee, or passing it to a native function, changes the answer. The external caller may choose another base. The compiler must retain `power` and give its parameters their declared bounds. @@ -52,8 +53,8 @@ must retain `power` and give its parameters their declared bounds. ## Publishing an object and exposing it to unknown code The construction chapter's `EscapeNode` publishes initialized fields into -ordinary public memory. The completed object has not, yet, escaped; it -depends on what happens to the `self` pointer. +ordinary public memory. That alone does not expose the completed object to +unknown code; exposure depends on what happens to the `self` pointer. Escape analysis starts at the boundary with unknown code. Returning an object across that boundary exposes its field aliases. An exposed field can contain @@ -67,7 +68,7 @@ The same rule applies to arguments passed to native code. Native code may keep an address after the call returns, or use it on a path that never returns. `Stop` therefore also collects exposure directly from reachable native calls. A call to a known Simple function instead passes the values and memory to that -function's graph; its uses SCCP to determine what escapes. +function's graph; SCCP follows those uses to determine what escapes. This is a conservative analysis by field alias. Two instances of the same struct share their field aliases. Exposing one may therefore expose addresses @@ -75,7 +76,7 @@ stored in the same fields of another. It does not identify each allocation separately. Another way to think of this: this analysis is what we get "for free" from just -running SCCP. We extend and sharpen the lattice a litte more, to allow more +running SCCP. We extend and sharpen the lattice a little more, to allow more precise tracking. @@ -89,17 +90,15 @@ precise tracking. `XInt`, already used for function sets, represents both. An empty set means no address has been discovered; a full set is the conservative unknown case. Meeting summaries unions the sets. Dual types complement them, so the existing -lattice operations and tests still apply. The `XInt` utilities themselves just -track collections of immutable bits. They have no indirection, they are just -fly-weight wrappers around `int[]`s. The help support shared and immutable -`int[]`s so that these bit sets follow the same invariants as the rest of the -Types. +lattice operations and tests still apply. The `XInt` utilities operate directly +on shared, immutable `int[]` arrays; they add no wrapper object. Interning lets +these sets follow the same sharing rules as the rest of the Types. Stores record addresses carried by their values. Memory Phis combine those facts over live paths. `MemMergeNode` gathers the slices whose aliases are exposed, rather than making every locally stored callback externally callable. If an exposed field reveals another alias, that slice participates as well. In -short, we follow escaping values through memory using `Types` using the formal +short, we follow escaping values through memory using the existing `Node.compute` and `Type` machinery. The hidden constructor helpers still keep fields in local SSA values and use @@ -121,8 +120,8 @@ Start -> callable functions -> returns and exposed memory -> Stop ``` Before SCCP, parsing and pessimistic iteration keep conservative callers and -memory. During SCCP, functions lose their unknown-caller edges unless their -function pointer escapes and become unknown entry points. Concrete calls then +memory. During SCCP, functions other than the chosen entry points lose their +unknown-caller edges unless their function pointers escape. Concrete calls rebuild the known call graph. Stop combines the results and memory visible to external callers. Those @@ -133,22 +132,24 @@ parameter inputs. That function can reveal further addresses through its own calls and returns. The ordinary worklist repeats until types, exposure sets, and call edges agree - i.e. the normal SCCP reaches a fixed point. -If you have a `main` function, it starts discovery. Without `main`, the code -is assumed to be a library, and all public entry points are fair game. +An explicit `main`, or one synthesized for a top-level return, starts discovery. +Without one, the code is treated as a library: ordinary named functions outside `sys` +seed the unknown entry points. Module privacy refines this rule in the next +chapter. Once SCCP finishes, you might have ordinary public functions callable from anywhere, or functions called via their name (direct field load holding the -function point), or via a function pointer thats only used in a few locations, +function pointer), or via a function pointer that's only used in a few locations, or it might escape in a region and get mixed with other function pointers, or you might never call a function at all. If a function is getting called, but -never publically, its arguments can sharpen across the `meet` of all call sites -- perhaps allowing not-null-ness to be tracked precisely and avoid some -explicit tests. - -After optimization, `FinalizeIR` freezes the external summary - the escaped -values from `Start/Stop`, for tracking if a function gets a public entry in the -ELF file. `FinalizerIR` does a few more cleanups, including removing the -`Stop->Start` edge SCCP uses to track escapes across external calls. +never from unknown code, its arguments can sharpen across the `meet` of all +call sites, perhaps proving an argument non-null and avoiding explicit tests. + +After optimization, `FinalizeIR` freezes the external summary from `Start/Stop` +and removes the `Stop->Start` edge SCCP uses to track escapes across external +calls. This summary says which functions need unknown callers; it does not +control ELF symbol visibility. The current ELF writer emits every retained +named function as a global symbol. ## Construction boundary to review diff --git a/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java b/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java index f22b73f1..19dd1c29 100644 --- a/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java +++ b/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java @@ -267,7 +267,7 @@ public class Parser { // Pre-call the function from Start, with worse-case arguments. This // represents all the future, yet-to-be-parsed functions calls and // external calls. - _scope.push(new Kind.Func()); + _scope.push(new Kind.Func(_privateFun!=null)); ctrl(fun); // Scope control from function // Unknown callers supply whole memory; field aliases are discovered lazily. _scope.mem(new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,new ProjNode(_code._start,1,ScopeNode.MEM0).peephole()).peephole()); @@ -768,6 +768,7 @@ public class Parser { // The alloc() call added the allocation scope !_scope.canInit(def) ) throw error("Cannot reassign final '"+name+"'"); + checkSharedField(def); // Parse assignment expression checkCaptureWrite(def); @@ -869,6 +870,14 @@ public class Parser { throw error("a not-null/non-zero expression"); t = expr._type; if( !xfinal ) t = inferredType(t); // Widen if not final + // A final field is still a local variable during construction. + // Its constant default is a value, not a one-value declaration + // type: later assignments and loop Phis may change it. + else if( _scope.inConstructor() && !(t instanceof TypeFunPtr) ) { + Type wide=inferredType(t); + t = t instanceof TypeMemPtr ptr && ptr.notNull() + ? TypeMemPtr.make((byte)2,((TypeMemPtr)wide)._obj,false).withAccess(ptr._ro) : wide; + } } // expr is a constant function if( t instanceof TypeFunPtr && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { @@ -948,7 +957,10 @@ public class Parser { Var v = _scope.var(index++); if( v.isFRef() ) continue; Node value = _scope.in(v); - boolean constant = v._final && v._init && value._type.isConstant(); + // Only function declarations are shared across instances. A final + // data field can change in a named or inline constructor, so a + // constant default does not justify folding every future read. + boolean constant = v._final && v._init && value._type instanceof TypeFunPtr && value._type.isConstant(); Field field = Field.make(v._name,v.type(),_code.getALIAS(),v._final,constant); shape=shape.add(field); TYPES.put(_nestedType,TypeMemPtr.make(shape)); @@ -1452,8 +1464,9 @@ public class Parser { if( name!=null ) { Var n = _scope.lookup(name); if( n != null && !(n.type() instanceof TypeMemPtr) ) { - if( n._final ) + if( n._final && !_scope.canInit(n) ) throw error("Cannot reassign final '"+n._name+"'"); + checkSharedField(n); Node expr = liftExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type(),true); _scope.update(n,expr); return expr; @@ -1545,8 +1558,9 @@ public class Parser { if( ch==0 ) // Normal primary, check for postfix updates return parsePostfix(rvalue); // Assign-update direct into Scope - if( var._final ) + if( var._final && !_scope.canInit(var) ) throw error("Cannot reassign final '"+var._name+"'"); + checkSharedField(var); checkCaptureWrite(var); Node op = opAssign(ch,rvalue, var.type() ); _scope.update(var,op); @@ -1600,6 +1614,24 @@ public class Parser { throw error("Cannot assign captured variable '"+var._name+"' in an initializer"); } + // Inline constructors have local field slots too, but shared function + // declarations have no instance slot to publish. Do not silently discard + // an assignment to one of those declarations. + private void checkSharedField(Var var) { + if( !_scope.canInit(var) ) return; + if( _scope.kind(var) instanceof Kind.Define ) { + checkPrivateWrite(var._name); + return; + } + for( int i=_scope.depth()-1; i>0; i-- ) + if( _scope._kinds.at(i) instanceof Kind.Alloc alloc ) { + Field field=alloc._tmp._obj.field(var._name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+var._name+"'"); + return; + } + } + // Check for assign-update, "x += e0". // Returns "x + e0"; the caller assigns value. // if ch is postfix, then lhs is kept and caller will unkeep and return it. @@ -1980,6 +2012,7 @@ public class Parser { if( v.type() instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) throw error("A constructor is only called by new"); if( matchOpx('=','=') ) { + checkPrivateWrite(name); Node value=parseAsgn().keep(); checkPublication(value); _scope.update(v,liftExpr(value,v.type(),false)); @@ -1990,6 +2023,7 @@ public class Parser { throw error("Cannot read uninitialized field '"+name+"'"); char ch=_lexer.matchOperAssign(); if( ch!=0 ) { + checkPrivateWrite(name); Node op=opAssign(ch,value,v.type()); checkPublication(op); _scope.update(v,op); @@ -2000,6 +2034,12 @@ public class Parser { return parsePostfix(value); } + private void checkPrivateWrite(String name) { + Field field=((TypeMemPtr)TYPES.get(_nestedType))._obj.field(name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+name+"'"); + } + // zero/sign extend. "i" is limited to either classic unsigned (min==0) or // classic signed (min=minus-power-of-2); max=power-of-2-minus-1. private Node zsMask(Node val, Type t ) { diff --git a/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 3c1e9776..f65a822d 100644 --- a/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -39,7 +39,10 @@ public class ScopeNode extends Node { // Basic block scoping public static class Block extends Kind { } // Function scope - public static class Func extends Kind { } + public static class Func extends Kind { + public final boolean _constructor; + public Func(boolean constructor) { _constructor=constructor; } + } // Struct definition, mapping types to fields public static class Define extends Kind { public Define(TypeMemPtr tmp) { _tmp=tmp; } public final TypeMemPtr _tmp; } // Struct allocation, mapping values to fields @@ -137,15 +140,25 @@ public class ScopeNode extends Node { } - // Allocation fields live in the scope immediately outside the constructor - // block. Nested blocks retain that permission; surrounding bindings do not. + // Fields are writable local values until construction ends, even when the + // published field is final. Ordinary methods and final local variables do + // not inherit that permission. This also decides which loop Phis we need. public boolean canInit(Var v) { - for( int i=_kinds._len-1; i>0; i-- ) - if( _kinds.at(i) instanceof Kind.Alloc ) + for( int i=_kinds._len-1; i>=0; i-- ) { + Kind k=_kinds.at(i); + if( k instanceof Kind.Alloc ) return v._idx >= _kinds.at(i-1)._lexSize && v._idx < _kinds.at(i)._lexSize; + if( k instanceof Kind.Define ) return kind(v)==k; + if( k instanceof Kind.Func fun ) + return fun._constructor && i>0 && _kinds.at(i-1) instanceof Kind.Define def && kind(v)==def; + } return false; } + private boolean loopVar(Var v) { + return !v._final || (canInit(v) && !v.type().isHighOrConst()); + } + public boolean inConstructor() { return _kinds.last() instanceof Kind.Define; } public boolean inAllocation () { return _kinds.last() instanceof Kind.Alloc ; } public boolean inFunction () { return _kinds.last() instanceof Kind.Func ; } @@ -294,7 +307,7 @@ public class ScopeNode extends Node { // Memory is one binding, with the same lazy loop Phi as scalar names. for( int i=1; i FREE = new Ary<>(Field.class); diff --git a/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java new file mode 100644 index 00000000..e91dc384 --- /dev/null +++ b/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -0,0 +1,63 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.StoreNode; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FinalConstructionTest { + private static final String LOOP = """ + val step={int x -> x*2+1;}; + struct C { + val x=1; + new C={int n -> + x=0; x=1; + for(int i=0; ix;}; + }; + return new C(arg).get(); + """; + + @Test public void testFinalConstructorLocals() throws java.io.IOException { + var code=new CodeGen(LOOP).driver(CodeGen.Phase.LocalSched); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("15",Eval2.eval(code,3)); + assertEquals("63",Eval2.eval(code,5)); + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("x") ) + assertEquals("Final field writes belong after the loop",0,st.cfg0().loopDepth()); + return null; + }); + String[] sources={ + "struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;", + "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i for(int i=0;ix=2.5;}; }; return new C().x;", + "struct C { val x=\"a\"; new C={int n -> for(int i=0;ix=new int[2]; x=new int[3];}; }; return new C().x#;" + }; + String[] results={"2","8","4","2.5","98","3"}; + for( int i=0; ival x=1; x=2;}; }; return new C();", + "struct C { val f={->1;}; new C={->f={->2;};}; }; return new C().f();" + }; + for( String src : rejected ) { + try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(src); } + catch( Parser.ParseException e ) { assertTrue(e.getMessage(),e.getMessage().startsWith("Cannot")); } + } + + var dir=java.nio.file.Path.of("build/objs"); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.smp"),"val buildFinal={int arg ->"+LOOP+"};"); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.c"),""" + extern long long buildFinal(long long); + int main(void) { return buildFinal(0)!=1 || buildFinal(3)!=15 || buildFinal(5)!=63; } + """); + TestC.run("build/objs","finalConstruction","",-1); + } +} diff --git a/chapter-incomplete-types/README.md b/chapter-incomplete-types/README.md index bfe0d44b..3ef144ed 100644 --- a/chapter-incomplete-types/README.md +++ b/chapter-incomplete-types/README.md @@ -7,8 +7,8 @@ English | [日本語](README.ja.md) The previous compiler solved for recursive types and calls, but its parser still needs types to choose e.g. float-vs-integer arithmetic instructions or -field tyoes. Forward references, or external references or even simple loop -structures can hide whats needed. +field types. Forward references, external references, or even simple loop +structures can hide what's needed. This chapter records the operations first and resolves their type-dependent parts later. Scalar types, numeric modes, conversions, guards, symbolic @@ -27,25 +27,25 @@ return add(new Later { x = 2.5; }); When parsing `p.x + 1`, the compiler knows the receiver's name (type `Later`), but not its fields! It makes a Load with a symbolic field offset `FldOff` and an `Add` whose numeric mode is still unset: might be an integer add or a -floatint point add. Later we parse `Later` which supplies the field and its +floating-point add. Later we parse `Later`, which supplies the field and its storage type. Type propagation then selects floating-point addition and converts the integer `1` operand. The result is some floating point math -`2,5 + (flt)1` which folds to `3.5`. +`2.5 + (flt)1`, which folds to `3.5`. Forward writes work the same way. Until a field is known, its Store carries whole memory - it aliases everything until we know what it aliases. Once resolved, the Store converts its value to the declared field type and updates that field's alias, which generally will result in a cascade of peepholes -sharpening the memory edges. The Store always keeps its control. Update -operators, e.g. ` x += y` work as syntax sugar over `x = x + y` (which might -need e.g. narrowing integer results when storing into a narrow `u8` field, or -not if storing into a `flt64` field). +sharpening the memory edges. Resolving the alias preserves the Store's control +input. Update operators such as `x += y` work like `x = x + y`, while evaluating +the destination only once. Storing into a `u8` field may narrow an integer +result; storing into an `f64` field may instead convert it to floating point. `FldOff` deserves more mention: it represents a named field reference (Load or -Store) to an unknown `TypeStruct`. In the above example, thats `p.x` : field -`x` from an unknown value of type `A{var x, ...}`. Since struct `A` can pick -up any more fields, and the field type itself is the unknown `var` type, -`FldOff` really knows very little about what's going on. Its more of a *shape* +Store) to an incomplete `TypeStruct`. In the example above, that's `p.x`: field +`x` of a `Later` whose fields are not yet declared. The struct can gain more +fields, and the field type itself is still unknown, so +`FldOff` really knows very little about what's going on. It's more of a *shape* placeholder, and awaits stronger types to peephole into a "real" field reference. @@ -54,7 +54,7 @@ reference. The parser always knows when it has *values* vs memory or control, but maybe not whether something is an int or a flt until much later. Instead it uses -`TypeScalar` for such this. `TypeScalar` contains integers, floats, object +`TypeScalar` for this. `TypeScalar` contains integers, floats, object pointers, and function pointers. These are classic *values* in the sense that they represent small concrete things that typically fit in a machine register. Values exclude control and memory. Scalar bottom says that a value's family is @@ -62,30 +62,31 @@ not known; its top represents an unreachable scalar result. A scalar union can still be tested for truth, even when its alternatives use different families; integers, floats, and pointers all carry a notion of zero. -A scalar Phi now meets its live inputs, it no longer carries a parser-supplied +A scalar Phi now meets its live inputs; it no longer carries a parser-supplied lower bound. An unfinished loop Phi starts at scalar bottom because its backedge is missing. Parameters retain their declared interface types, and -memory Phis retain their memory role. Loop, parameter, and recursive-return +memory Phis retain their memory role. Loop Phis, Parms, and recursive CallEnds still do widening. ## Record conversions and branch facts The parser ends up inserting `Convert` nodes where it used to directly convert -values. This includes widening integers to match float operators, narrowing -when storing into narrow integer fields, or widening when loading from them. -A bad load or store still produces bottom. Type checking errors at conversions -simply "pass the buck" to their broken input. +values. This includes converting integers to match float operators and narrowing +values stored into integer fields. Loads derive their value types from memory; +they no longer impose a parser-supplied bound. A bad load or store still produces +bottom. Type checking looks through a conversion with an already-broken input +so it can report the original error. -`Convert` records an known type destination, such as a local declaration or +`Convert` records a known destination type, such as a local declaration or function parameter. It waits for its input type to sharpen, and then peepholes -into a widening, truncation, permission check, or no operation. +into a widening, truncation, permission check, or no operation. `Guard` records a branch fact: zero/null or nonzero/non-null. Its value family comes from its input when known. The control edge keeps the fact on the path where it was proved. A guard output is the same runtime value as its input; it emits no instruction, just the sharpened type result. Guards are typically -used track not-null pointer tests across large areas. +used to track not-null pointer tests across large areas. Arithmetic nodes carry a `mode`: a 3-state enum tracking an unset, integer, or floating-point mode. Known inputs can settle it during peepholes. SCCP @@ -101,24 +102,24 @@ val foo = { A a -> a.b; }; struct A { B b; }; ``` -In this code, when parsing `foo` we do not know what `a` nor `a.b` will be, -other than there is a `b` field load on `a`, so `a` is a `TypeStruct` of `A`. +When parsing `foo`, we know that `a` points to an `A`, but not the fields of +`A` or the type of `a.b`. The load records that `A` must have a field named `b`. Forward shapes (`A`) remain *open* until their declaration, as in prior -chapters. When parsing `struct A` we find `B`, but `B` here is never declared... and +chapters. When parsing `struct A` we find `B`, but `B` here is never declared... and does not need to be. After parsing, the compiler *closes* all declarations as a group, including -types cycles (such as a `struct LinkedList` having a `next` field typed as a -`LinkedList`), and through function signatures. It then replaces old types -shapes in cached node types and stored declarations, (preserving pointer -permissions and nullability). This gives us a simple, clean *complete* version -of all mentioned types, no partially-made types from early in the parse remain. +type cycles (such as a `struct LinkedList` having a `next` field typed as a +`LinkedList`) and references through function signatures. It then replaces old +type shapes in cached node types and stored declarations, preserving pointer +permissions and nullability. Declared types now have their complete field sets. +An undeclared type such as `B` may remain opaque: this example only passes its +pointer around and does not need to allocate a `B` or access its fields. -We do not know if the program will type-check yet, but at least we know what -the types *are*. +Closing declarations does not itself prove the program will type-check. -At this point `FldOff` can being to peephole into a `ConFldOff` - moving -from a purely symbol field reference of an unknown type to a concrete +At this point `FldOff` can begin to peephole into a `ConFldOff` - moving +from a symbolic field reference of an incomplete type to a concrete field of a known type. Later, after structures get a *layout*, `ConFldOff` will become a concrete field offset, a number like `4` or `8`. diff --git a/chapter-interprocedural-sccp/README.ja.md b/chapter-interprocedural-sccp/README.ja.md index 4dec73c7..e9e57c6d 100644 --- a/chapter-interprocedural-sccp/README.ja.md +++ b/chapter-interprocedural-sccp/README.ja.md @@ -14,11 +14,11 @@ **Simple** では、ここでもほぼ何もせず、SCCP を手続き間で実行します。`CallNode` は `FunNode` を呼び、`ReturnNode` は `CallEndNode` に戻り、まったく通常の方法で型情報を渡します。ただし問題が1つあります。`Call` と `Fun`、および `Return` と `CallEnd` の間に辺がありません。そのような辺が [呼び出しグラフ](https://en.wikipedia.org/wiki/Call_graph) を構成します。 -通常の SCCP を拡張し、`Call` の関数ポインタ入力が変わり、新しい関数対象を得る時点を監視します。対象の数は有限なので、前の章で説明した *関数インデックス* で正確に表します。分割コンパイルでは、このコンパイル単位の外で呼ばれる無限の未知関数に対して、特別なインデックスを追加します。 +通常の SCCP を拡張し、`Call` の関数ポインタ入力が変わり、新しい関数対象を得る時点を監視します。対象の数は有限なので、前の章で説明した *関数インデックス* で表します。未知の対象には保守的な要約が必要です。後の分割コンパイルでは、オブジェクトファイルをまたいで関数の同一性も保ちます。 -`Call` で新しい *fidx* を見つけたら、`Call` と `Fun`、`Return` と `CallEnd` を *リンク* します。このリンクは、`Call` から `Fun`、そして `Call` の引数から `Fun` の `ParmNode` へ CFG 辺を追加したかのように動きます。`FunNode` は `RegionNode` を、`ParmNode` は `PhiNode` を拡張するため、同じ意味規則を使います。`Fun` は呼ばれると到達可能になり、`Parm` はすべての呼び出し引数をマージ(*meet*)します。同様に `CallEnd` は、すべての `Return` の *meet* を計算して、呼ばれたすべての関数の結果をマージします。 +`Call` で新しい *fidx* を見つけたら、`Call` と `Fun`、`Return` と `CallEnd` を *リンク* します。このリンクは、`Call` から `Fun` へ制御辺を、`Call` の引数から `Fun` の `ParmNode` へデータ辺を追加したかのように動きます。`FunNode` は `RegionNode` を、`ParmNode` は `PhiNode` を拡張するため、同じ意味規則を使います。`Fun` は呼ばれると到達可能になり、`Parm` はすべての呼び出し引数をマージ(*meet*)します。同様に `CallEnd` は、すべての `Return` の *meet* を計算して、呼ばれたすべての関数の結果をマージします。 -この *リンク* 段階はアルゴリズムの内部ループにいくつかの検査を加え、正確な呼び出しグラフも構築し、SCCP の手続き間実行を可能にします。進行中に新しい辺を少し発見する以外は、中核アルゴリズムは変わりません。不動点解で停止することが保証され、たとえば特定の呼び出しパラメータが、すべての呼び出し経路で定数、あるいは非 null と発見できることがあります。 +この *リンク* 段階はアルゴリズムの内部ループにいくつかの検査を加え、型伝播で発見した対象から保守的な呼び出しグラフを構築します。進行中に新しい辺を少し発見する以外は、中核アルゴリズムは変わりません。不動点解で停止することが保証され、たとえば特定の呼び出しパラメータが、すべての呼び出し経路で定数、あるいは非 null と発見できることがあります。 ## 再帰もループ diff --git a/chapter-interprocedural-sccp/README.md b/chapter-interprocedural-sccp/README.md index 7310de80..d17f937c 100644 --- a/chapter-interprocedural-sccp/README.md +++ b/chapter-interprocedural-sccp/README.md @@ -3,11 +3,11 @@ English | [日本語](README.ja.md) [Previous: SCCP Within a Function](../chapter-sccp/README.md) | -[Next: SSA Before Types Are Known](../chapter-incomplete-types/README.md) | +[Next: SSA Before Types Are Known](../chapter-incomplete-types/README.md) -The previous chapter runs SCCP with each function, and assumes very +The previous chapter runs SCCP within each function, and assumes very conservative typing across calls. This chapter lets the same worklist -discover call edges and propagate values between functions. +discover call edges and propagate values between functions. ## Discovering calls while solving types @@ -20,21 +20,21 @@ Such edges make up a [Call Graph](https://en.wikipedia.org/wiki/Call_graph). We extend the normal SCCP algorithm to observe when a `Call`s function pointer input changes - when it picks up new function targets. There are only a finite number so we represent them exactly with *function indices*, or *fidx* -internally, as described earlier chapters. (Separate compilation adds a -special index for the infinite unknown functions that are called outside this -compilation unit). +internally, as described in earlier chapters. Unknown targets still require +conservative summaries. Later, separate compilation must also preserve function +identities across object files. Once we observe a new *fidx* at a `Call`, we *link* the `Call` with the `Fun`, and the `Return` with the `CallEnd`. Such linkage acts exactly as-if we added -a CFG edge from the `Call` to the `Fun`, and the `Call`s arguments to the -`Fun`s `ParmNodes`. `FunNodes` extend `RegionNodes` (and `ParmNodes` extend +a CFG edge from the `Call` to the `Fun`, and data edges from the `Call`s arguments +to the `Fun`s `ParmNodes`. `FunNodes` extend `RegionNodes` (and `ParmNodes` extend `PhiNodes`), so this linkage uses the same semantics. `Fun`s become reachable when called; `Parm`s merge (*meet*) all calling arguments, and so on. Similarly, `CallEnd`s merge results from all called functions, computing the *meet* over all the `Return`s. This *link* step adds a few checks to our algorithm's inner loop, and also -builds a precise Call Graph which allows our SCCP to run interprocedurally. +builds a conservative Call Graph from the targets found by type propagation. Other than discovering a few new edges as the algorithm proceeds, the core algorithm is unchanged. We are guaranteed to terminate with a fixed point solution, and may discover e.g. certain call parameters are constants (or @@ -42,25 +42,26 @@ e.g. not-null) on all calling paths. ## Recursion is a loop in disguise -We use the notion of *widening* integer ranges at loop heads, see Chapter XXX. +We use *widening* of integer ranges at loop heads, as described in the +[previous chapter](../chapter-sccp/README.md#convergence-and-calls). This allows us to have a sharper notion of integers over `BOTTOM` without also -allowing the lattice to fall 2^32 times. The same issue arises during +allowing the range to grow one integer at a time. The same issue arises during recursive calls: an argument can flow from a Parm through arithmetic into a recursive call, then back to the same Parm. ```simple -val fact = { int x -> x ? 0 : f(x-1)+1; }; +val f = { int x -> x == 0 ? 0 : f(x-1)+1; }; ``` -Here if we pass in `f(999)` , parameter `x` will have the integer range -`[999]`. We'll then track the recursive call to `f(998)` via normal SCCP -and realize the parameter `x` has a range `[999-998]`. Lather, rinse, repeat -and we're at `[999-997]`. We need to *widen* integer parameters at `Parms` -not just loop-head `Phis`. +If the only outside call is `f(999)`, parameter `x` starts with the range +`[999,999]`. We'll then track the recursive call to `f(998)` via normal SCCP +and discover that `x` has range `[998,999]`. Lather, rinse, repeat, +and we're at `[997,999]`. We need to *widen* integer parameters at `Parms`, +not just loop-head `Phis`. The same issue arises during the recursive return: the first return will be -with `0` when `x` is zero, then we return `0+1` or the range `[0-1]` then range -`[0-2]`, etc. `CallEnds` also need to widen. +with `0` when `x` is zero, then we return `0+1` or the range `[0,1]`, then range +`[0,2]`, etc. `CallEnds` also need to widen. Loop Phis, Parms, and CallEnds get widening points, and since this same issue hits in 3 unrelated places, their comments all cross-reference each other. @@ -72,7 +73,7 @@ A `FunPtrNode` (function address) that survives outside a direct call may be called by code we cannot see. We have to assume the address escapes into unknown code, which later calls it. We preserve its unknown-caller input, since we cannot tell what that unknown caller will pass. Precise escape -analysis is introduced later exactly to inmprove this. +analysis is introduced later to improve this. ## Building and checking diff --git a/chapter-modules/Makefile b/chapter-modules/Makefile new file mode 100644 index 00000000..e89e1bd7 --- /dev/null +++ b/chapter-modules/Makefile @@ -0,0 +1,201 @@ +# +# Makefile expects to run at the project root, and not nested inside anywhere +# +# cd $DESK/Simple/chapterXX; make +# +# +# first time only - this is the only thing that hits the Interwebs +# +# make lib +# +# any time you want to nuke everything from space: +# +# make clean tags +# +# run the tests +# +# make tests + +####################################################### +# +# Boilerplate because make syntax isn't the best +# + +# Use bash for recipes +SHELL := /bin/bash + +# Remove target on error (no broken leftover files) +.DELETE_ON_ERROR: + +# Keep partial builds but not partial recipes; keeping them speeds up the next `make` +.NOTINTERMEDIATE: + +# 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 $(SRC)/$(SIMPLE)/node/cpus/*/*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)) +include $(firstword $(wildcard ../print/print.mk print/print.mk)) +include $(firstword $(wildcard ../isa/isa.mk isa/isa.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 := $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag +# Optionally add ctags to the default target if a reasonable one was found. +ifneq ($(CTAGS),) +default_targets += tags +endif + +default: $(default_targets) + +####################################################### +# Compile the chapter and shared viewer sources together + +$(CLZDIR)/main/.mtag: $(main_javas) Makefile + @[ -d $(CLZDIR)/main ] || mkdir -p $(CLZDIR)/main + @# This crap is really just "javac", but with a beautiful error message. + @# The very very long list of java files is suppressed and counted. + @# The required output "Note: blah blah deprecated blah" is suppressed. + $(file >.argsM.txt,$(main_javas)) + @if [ ! -z "$(main_javas)" ] ; then \ + echo "compiling main because " $< " and " `wc -w < .argsM.txt` " more files" ; \ + if ! javac $(JAVAC_ARGS) -cp "$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(SRC) -d $(CLZDIR)/main $(main_javas) >& .out.txt ; then \ + cat .out.txt ; \ + exit 1; \ + fi ; \ + rm -rf .out.txt ; \ + fi + @touch $(CLZDIR)/main/.mtag + @rm -f .argsM.txt + +$(main_classes): $(CLZDIR)/main/.mtag + + + +####################################################### + +OODT := $(isa_test_javas) +$(CLZDIR)/test/.ttag: $(test_classes) $(test_javas) $(isa_test_javas) $(CLZDIR)/main/.mtag + @[ -d $(CLZDIR)/test ] || mkdir -p $(CLZDIR)/test + $(file >.argsT.txt,$(OODT)) + @if [ ! -z "$(OODT)" ] ; then \ + echo "compiling test because " $< " and " `wc -w < .argsT.txt` " more files" ; \ + if ! javac $(JAVAC_ARGS) -cp "$(CLZDIR)/test$(SEP)$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(TST) -d $(CLZDIR)/test $(OODT) >& .out.txt ; then \ + cat .out.txt ; \ + exit 1; \ + fi ; \ + rm -rf .out.txt ; \ + fi + @touch $(CLZDIR)/test/.ttag + @rm -f .argsT.txt + +# Collect just the out-of-date files +$(test_classes): $(CLZDIR)/test/%class: $(TST)/%java + $(eval OODT += $$<) + +####################################################### +# Base launch line for JVM tests +JVM=nice java -ea -cp "$(CLZDIR)/main${SEP}${jars}${SEP}$(CLZDIR)/test" + + +tests: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @echo "testing " $(test_cp) + @[ -d build/objs ] || mkdir -p build/objs + @$(JVM) org.junit.runner.JUnitCore $(test_cp) + @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap + +fuzzer: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @echo "fuzzing " $(test_cp) + @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap + +# Build a Simple jar +release: build/release/simple.jar + +# Build a Simple jar +build/release/simple.jar: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @echo "jarring " $@ " because " $? + @[ -d $(dir $@) ] || mkdir -p $(dir $@) + @jar cf build/release/simple.jar -C $(CLZDIR)/main . -C $(CLZDIR)/test . -C $(SRC)/$(SIMPLE) . -C $(TST)/$(SIMPLE) . + + +.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) + +spill-stats: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @[ -d build/objs ] || mkdir -p build/objs + @$(JVM) com.seaofnodes.simple.SpillStats + +.PHONY: spill-stats + +.PHONY: build +build: $(CLZDIR)/main/.mtag + +# The ABI shim gives standalone native tests the same entry contract as clients. +CPU_PORT := x86_64_v2 +ifeq ($(OS),Windows_NT) +CALL_CONV := win64 +NATIVE_ABI := ms_abi +else +CALL_CONV := SystemV +NATIVE_ABI := sysv_abi +endif +runtime_obj := build/release/$(CPU_PORT)_$(CALL_CONV)/simple_crt.o +$(runtime_obj): src/main/c/simple_crt.c Makefile + @mkdir -p $(dir $@) + gcc -c -O2 -DCALL_CONV=$(NATIVE_ABI) $< -o $@ +tests: $(runtime_obj) diff --git a/chapter-modules/README.ja.md b/chapter-modules/README.ja.md new file mode 100644 index 00000000..3765eeff --- /dev/null +++ b/chapter-modules/README.ja.md @@ -0,0 +1,91 @@ +# モジュールと遅延クラス初期化 + +[English](README.md) | 日本語 + +[前: エスケープ解析と未知の呼び出し元](../chapter-escape/README.ja.md) + +この章では複数のソースファイルを一度にコンパイルします。各ファイルは +一つのクラスと共有オブジェクトを定義し、トップレベルの文は隠れた +`` 関数になります。最初の能動的な使用で初期化し、その後は +同じ結果を使います。クラスへの参照を保持するだけでは初期化しません。 + +## 必要なときだけ表を作る + +実行できる例は [`src/test/smp/lazy`](src/test/smp/lazy) にあります。 +`App.smp` は Main、Tables、Stats への参照を公開します。 +`App/Stats.smp` の `builds` は初期化回数を数えます。 +`App/Tables.smp` は次のように表を構築します。 + +```simple +Stats.builds++; +int[] _squares=new int[256]; +for(int i=0; i<256; i++) _squares[i]=i*i; +val square={u8 n -> _squares[n];}; +``` + +`App/Main.smp` が入口です。 + +```simple +return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds; +``` + +引数が 0 なら結果は 0 で、Tables は初期化されません。引数が 1 なら +9 + 16 + 100 = 125 です。二度の呼び出しでも表は一度しか作りません。 +`_squares` は非公開のフィールドで、公開関数を通して読み出します。 +完全なファイル一覧と評価用 Java コードは [英語版](README.md) を参照してください。 + +## ファイルと解析の境界 + +`ParseAll` は未解決のクラス名や型から必要なソースを探します。 +ディレクトリ階層が名前空間になり、`App/Main` から `Tables` は兄弟の +`App/Tables` を指せます。親の初期化は子より先なので、親も解析します。 +相互参照する型はソースの発見後に閉じます。 + +`CompUnit` はソース、クラス型、関数をまとめます。各単位には +`StartCU` と `StopCU` があり、全体の Start/Stop の下で解析します。 +公開関数には未知の呼び出し元が存在します。非公開の関数は引き続き +エスケープ解析によって特殊化できます。フィールド名の先頭の `_` は +非公開を表します。ただし、現実装はこれを公開インターフェースの選択に使うだけで、 +兄弟クラスからの直接アクセスをまだ拒否しません。このアクセス検査は未実装です。 + +標準ライブラリ `sys` もソースから解析します。ローカルの `sys` が +なければ `src/main/smp` を使います。ネイティブの `"C"` 宣言は従来通り +ネイティブリンカーで解決します。 + +## 初期化の順序と循環 + +クラスのフィールドの読み書き、メソッド呼び出し、インスタンス生成は +初期化を必要とします。パーサーは `CLInit` を挿入し、最適化中は制御と +メモリ効果を持つ不透明なノードとして残します。実行後は隠れた `$init` +フラグが真なので、通常のメモリ最適化で重複した検査を除去できます。 + +型やクラスへの参照の循環は合法です。しかし A の初期化が B を使い、 +B の初期化が A を使う場合、先に完了できるクラスがありません。 +`ClassInitDependencies` は最適化後の呼び出しと `CLInit` をたどり、 +複数のクラス初期化を含む循環をソース位置付きで報告します。 +通常の再帰や同じ初期化関数内の再帰は許されます。 + +検査後、`Lower` はフラグ検査と未初期化時の呼び出しに展開します。 +親を先に初期化し、フラグは初期化関数の入口で真にします。同じクラスのヘルパーから +再び使えるようにするためです。他クラスを経由して未完成のクラスへ戻る循環は拒否します。 +初期化関数 A が B の初期化を完了させてから再開することは可能です。 + +## コード生成とビルド + +この章では必要なソースをすべて同時にコンパイルします。`FinishCalls` +が解析用の呼び出し辺を切り離し、`StaticData` がクラスの共有領域と +定数フィールドを配置します。ELF は発見したソースのうち生きているコードを含みますが、 +理想 IR のシリアライズ情報は含みません。 + +リポジトリのルートで次を実行します。 + +```sh +make tests CHAPTERS=chapter-modules +``` + +このディレクトリでは初回に `make lib`、続いて `make tests` を使えます。 +`make view` は共有グラフビューアーを起動します。 + +次の章ではライブラリのソースがなくてもコンパイルできるように、型と +理想 IR の保存、インポート時の識別子変換、インポート先を含む初期化検査 +を追加します。ライブラリ構築と最適化の調整は付録として扱います。 diff --git a/chapter-modules/README.md b/chapter-modules/README.md new file mode 100644 index 00000000..f0fac494 --- /dev/null +++ b/chapter-modules/README.md @@ -0,0 +1,271 @@ +# Modules and Lazy Class Initialization + +English | [日本語](README.ja.md) + +[Previous: Escape Analysis and Unknown Callers](../chapter-escape/README.md) + + +The compiler can now discover several source files and compile them together. +Each file defines a class with one shared class object; files can nest in +directories forming a module structure. + +The shared class object is a normal object with fields holding function pointers +or other state. The top-level code in the file forms the `` +function, which runs once only on the first active use of the class - i.e., +lazy class initialization. An initializer can trigger another class's +initialization and wait for it to finish. It cannot observe that other class +half-initialized. Cycles between class initializers are not allowed. + +This chapter adds file and class semantics to the escape analysis we already +have. All required Simple source is available in one compilation. + + +## Example: Lazy class initialization, or Pay-As-You-Go + +Suppose an application sometimes needs a `Table` but not often; many runs may +never need it. Putting its construction in the application entry would make +every run pay (the eager-init model). Putting it in a getter would need a test +and cached storage. A class initializer provides `Table` lazily, on demand, +and once for all users of the class. This is similar to the Java class init +model except nothing ever sees a class mid-initialization, except the class +itself. + +The runnable example is in [`src/test/smp/lazy`](src/test/smp/lazy). +Its directory tree also defines the namespace: + +```text +App.smp +App/Main.smp +App/Stats.smp +App/Tables.smp +``` + +`App.smp` names its children: + +```simple +val Main=Main; +val Tables=Tables; +val Stats=Stats; +``` + +These are class references being stored into normal fields, to give easy access +to nested classes. Merely storing the reference does not trigger the class +initializer. + +`App/Stats.smp` supplies a visible count: + +```simple +int builds=0; // Static global counter +``` + +`App/Tables.smp` builds the table and exposes a function that reads it; the +array is marked private and its public function is the intended interface +(see the current access-check limitation below): + +```simple +Stats.builds++; // Number of table builds... only ever zero or 1! + +// Build a private table of squares +int[] _squares = new int[256]; +for(int i=0; i<256; i++) _squares[i]=i*i; + +// Public accessor to get a square +val square = { u8 n -> _squares[n];}; +``` + +`App/Main.smp` is the main entry: + +```simple +return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds; +``` + +With `arg == 0`, the result is 0 and `Tables` never initializes. With `arg == 1`, +the result is 125: the two lookups produce 9 and 16, and the table is built once. +From this chapter directory, the test driver can evaluate it: + +```sh +java -ea -cp "build/classes/main:build/classes/test:lib/*" \ + com.seaofnodes.simple.Simple --root src/test/smp/lazy --eval App/Main.smp 1 +``` + +On Windows, use `;` instead of `:` between classpath entries. + +The evaluator can also run the example directly after type checking: + +```java +CodeGen code=new CodeGen("src/test/smp/lazy", "build/objs", "App/Main", + null, 123, true, TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); +Eval2.eval(code,0); // "0" +Eval2.eval(code,1); // "125"; a fresh evaluation starts a fresh program +``` + +`BoundaryTest.testLazyTables` checks these results. `ClassInitDependenciesTest` +also runs native code that calls a class repeatedly in one process, checking +both the cold and already-initialized paths. + +### Lazy initialization details + +The implementation tracks a hidden `$init` variable in every class. It changes +from `false` to `true` at initializer entry, allowing the class's own helpers +to use it while initialization is in progress. The separate cycle check prevents +another class's initializer from reaching back into that unfinished class. +A check before each active use calls the initializer if needed. The optimizer +tracks the flag as a normal alias, distinguishes `false`, `true`, and `bool`, +and can sometimes remove the test and call. + + +## Name lookup + +If a name misses in the file scope, the parser looks for a matching Simple +source file under the module root. It searches the current class's children, +then enclosing scopes. For example, in `App.smp`, the declaration +`val Main=Main;` defines a normal field, but the right-hand `Main` is unresolved. +The compiler finds `App/Main.smp`, which defines the referenced class. + +`ParseAll` starts with the requested file and follows unresolved class names +and types to source files. From `App/Main`, `Tables` can name the sibling +`App/Tables`; a longer name walks down the class tree. Long dotted paths are +also allowed: `App.Tables`. The parser also reads parents, since their +initialization must precede their children. Cyclic source references are +allowed: the compiler closes recursive types after discovery. + +`CompUnit` ties a source file, class type, and function graph together. They +form a tree that mirrors the source directories and tracks which files need +loading and parsing. Each `CompUnit` has a `StartCU` and `StopCU`, beneath +the whole program's `Start` and `Stop`. + +### Private + +A leading underscore marks a field or class as private. The intended rule is +that private fields are visible only inside their declaring class, not to +siblings or parents. A private enclosing class also keeps its children out of +the public interface. + +The current compiler uses these names to choose the public interface and seed +escape analysis, but does not yet enforce the source access rule. For example, +a sibling can currently read `A._secret`. Rejecting that access is still needed; +the underscore alone is not an enforced boundary in this snapshot. + +Like in other languages, private fields allow you to hide implementation +details. They can be exposed via the normal getter/setter route or any other +way that makes sense. Escape analysis can track private fields more precisely +when all uses stay in known code, helping the usual optimizations. + +### Other names + +The bundled `sys` library uses the same source mechanism. It is a normal module +in all ways except that lookup falls back to the bundled `src/main/smp` when +no local `sys` is found. Native `"C"` declarations still name functions +or data resolved by the native linker. + + +## Active use and initialization order + +Reading or writing a class field, calling its method, or constructing an +instance needs the class initialized; a class reference alone does not. The +parser inserts `CLInitNode` at these active uses, and uses the usual collection +of peepholes to optimize these away. E.g. a forward reference *might* be a +class reference, so a `CLInit` is conservatively inserted. Later the fref +resolves to a normal instance... so the `CLInit` will peephole away. +`CLInit` has peeps for removing repeated checks; for telling that the check +must always be `true` or `false`, etc. + + +Parents initialize before children. In the example, `App` initializes before +`App.Main` and `App.Tables`. Its passive references to the children do not run +them while `App` is still being initialized. + +The compiler keeps `CLInit` opaque through optimization. It carries control +and whole-memory effects, plus the fact that this class's hidden `$init` flag +is `true` afterward. At the `Lower` pass, the `CLInit` explodes into a true +CFG diamond guarding the `` call. At this point we end up with a real +runtime guard check, i.e. real overhead. Removing redundant checks reduces +that cost; the first active use may still need a runtime check. + + +## Reject cycles that would observe unfinished classes + +An initializer can use a different class after that class finishes initializing. +For example, `A` can start, initialize `B`, then resume. A cycle would instead +ask one of them to use the other before it finishes. + +Passive references do not create initialization dependencies. These two sibling +files can refer to each other: + +```simple +// A.smp +val b = B; +// B.smp +val a = A; +``` + +Reading each other's fields during initialization does create a cycle: + +```simple +// A.smp +int value = B.value; +// B.smp +int value = A.value; +``` + +Parents add another dependency: a parent must finish before its child starts. +A parent initializer therefore cannot actively use its child. It can keep a +passive reference, or define a helper that uses the child when called after +the parent finishes. Calling that helper during parent initialization would +still create a cycle. + +The resulting dependency graph is a DAG, but initialization remains lazy. +Execution respects the dependencies of each active use; classes that are never +actively used need not initialize. + + +After optimization and during `TypeCheck`, `ClassInitDependencies` follows +surviving `CLInit` markers and calls. It checks strongly connected components +and reports cycles involving more than one class initializer. Ordinary +recursion and recursive helpers within a single initializer are allowed. Since +this is after SCCP, eliminated code cannot make a cycle. Surviving calls are +checked conservatively, even if a more powerful analysis could prove them dead. + +After checking, `Lower` expands each remaining marker into a flag test and a +call on the cold path. Parent initialization comes first. The initializer itself +sets the flag. Generated calls have conservative memory effects and do not +reopen SCCP's analysis of the optimized bodies. + + +## Emitting code and data + +After cleanup by `FinishCalls`, the live code and data from the discovered +sources are emitted in one ELF file. Final constant fields are emitted as +static data in the ELF using the helper `StaticData`; commonly this includes +static class fields holding function pointers, but any constant data can be +stored here. These fields then do not need any `` code. + +Mutable field initialization remains in ``, even if the initial value +is a constant. A final binding whose value is computed at runtime also needs +initializer code. For example, in `Theme.smp`: + +```simple +{int} read_setting = "C"; // Supplied by the native application +val scale = read_setting(); // Final binding; value only known at runtime +``` + + +## Build and review + +From the repository root: + +```sh +make tests CHAPTERS=chapter-modules +``` + +Or, in this directory, `make lib` once for JUnit, then `make tests`. The suite +covers source discovery, initialization cycles and parent order, native cold/hot +calls, shared class storage, constructor reads and loops, private callbacks, +and x86/ARM/RISC-V code generation. `make view` starts the shared graph viewer. + +The next chapter will keep this language behavior while allowing a client to +compile without the library's sources. That requires saving types and ideal IR, +remapping identities on import, and extending the same initialization checks +across imported bodies. Library construction and optimizer tuning remain +appendix topics in the [approved split](../docs/chapter-split.md). diff --git a/chapter-modules/pom.xml b/chapter-modules/pom.xml new file mode 100644 index 00000000..ac516e0b --- /dev/null +++ b/chapter-modules/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + com.seaofnodes + simple + 1.0 + + chapter-modules + jar + Modules and Lazy Class Initialization + + ${project.basedir}/../graph + ${project.basedir}/../print + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + shared-isa-sources + generate-sources + add-source + + + ${project.basedir}/../isa/src/main/java + + + + + shared-isa-test-sources + generate-test-sources + add-test-source + + ${project.basedir}/../isa/src/test-support/java + + + + + + + diff --git a/chapter-modules/src/main/c/simple_crt.c b/chapter-modules/src/main/c/simple_crt.c new file mode 100644 index 00000000..8656a3cc --- /dev/null +++ b/chapter-modules/src/main/c/simple_crt.c @@ -0,0 +1,61 @@ +#include +#include +#include +#include + +// errno may be thread-local or a macro calling a platform accessor. +int32_t simple_errno(void) { return errno; } + +// Native process arguments converted to ordinary Simple array layouts. +// The outer array is mutable; every contained byte array is published as +// immutable and non-null to Simple code. +typedef struct SimpleBytes { + uint32_t len; + unsigned char bytes[]; +} SimpleBytes; + +typedef struct SimpleArgs { + uint32_t len; + uint32_t pad; + SimpleBytes *args[]; +} SimpleArgs; + +__attribute__((CALL_CONV)) +extern int64_t simple_main(SimpleArgs *args); + +static SimpleArgs *simple_args(int argc, char **argv) { + if (argc < 0 || (uint64_t)argc > UINT32_MAX) + return NULL; + + SimpleArgs *args = calloc(1, sizeof(*args) + (size_t)argc * sizeof(args->args[0])); + if (args == NULL) + return NULL; + args->len = (uint32_t)argc; + + for (int i = 0; i < argc; i++) { + size_t len = strlen(argv[i]); + if (len > UINT32_MAX) { + free(args); + return NULL; + } + SimpleBytes *str = malloc(sizeof(*str) + len); + if (str == NULL) { + for (int j = 0; j < i; j++) free(args->args[j]); + free(args); + return NULL; + } + str->len = (uint32_t)len; + memcpy(str->bytes, argv[i], len); + args->args[i] = str; + } + return args; +} + +int main(int argc, char **argv) { + SimpleArgs *args = simple_args(argc, argv); + if (args == NULL) + return 1; + // The language-level hidden receiver is not part of the emitted ABI for + // free functions; source argument zero occupies native argument zero. + return (int)simple_main(args); +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/IterPeeps.java b/chapter-modules/src/main/java/com/seaofnodes/simple/IterPeeps.java new file mode 100644 index 00000000..e65ef6d9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/IterPeeps.java @@ -0,0 +1,349 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Opto; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.node.*; +import java.util.Arrays; +import java.util.BitSet; +import java.util.IdentityHashMap; +import java.util.Random; + +/** + * The IterPeeps 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 neighbors 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. It's 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 `IterPeeps.iterate` as `assert + * progressOnList(stop);`
  • + *
+ */ +public class IterPeeps { + + public final WorkList _work; + final WorkList _workInline; + private int _cnt; + + public IterPeeps( long seed ) { + _work = new WorkList<>(seed); + _workInline = new WorkList<>(seed); + } + + @SuppressWarnings("unchecked") + public N add( N n ) { return (N)_work.push(n); } + + public void addAll( Ary ary ) { _work.addAll(ary); } + + /** + * Iterate peepholes and inlining to a fixed point + */ + public void iterate( CodeGen code ) { + boolean didInline = false; + Ary defer = new Ary<>(Node.class); + while( true ) { + // Clean up everything that does not grow the code + iteratePeeps(code); + + // Pick an inline candidate, no real heuristic, first come, first served + boolean inlined = false; + CallEndNode cend; + while( (cend=_workInline.pop()) != null) { + if( cend.isDead() ) continue; + byte inline = cend.maybeInline(); + if( inline == -1 ) defer.add(cend); + if( inline > 0 ) { + inlined = true; + break; + } + } + // Inlined, run peeps until clean again + if( inlined ) { didInline = true; continue; } + // No more candidates, check the defer list + if( !didInline ) break; // No more progress, so all the "maybe inline after cleanup" do not progress + // Some inlining happened, retry all the "try again after cleanup" calls + didInline = false; + code.addAll(defer); + defer.clear(); + } + } + + // Run all the code-reduction and type-lifting peeps as possible + private void iteratePeeps( CodeGen code ) { + assert !CodeGen.expensiveAssert(0) || (progressOnList(code, _work) && schedulableUses(code)); + + Node n; + while( (n=_work.pop()) != null ) { + if( n.isDead() ) continue; + _cnt++; // Useful for debugging, searching which peephole broke things + var obs = code._obs; // Collect peephole neighbors for graph display + if( obs != null ) obs.before(n); + Node x = n.peepholeOpt(); + if( n instanceof CallEndNode cend ) + _workInline.push(cend); + if( x != null ) { + assert !x.isDead(); // Peepholes return alive answers + // 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); + // A self-returning peephole can have rewritten its input + // edges. The new defs gained a user and may have + // backwards, user-sensitive peepholes of their own. + for( Node z : x._inputs ) _work.push(z); + // 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); + for( Node z : x._outputs ) _work.push(z); + n.subsume(x); + } + } + // If there are distant neighbors, move to worklist + n.moveDepsToWorklist(); + // Very expensive assert. + assert !CodeGen.expensiveAssert(_cnt) || (progressOnList(code, _work) && schedulableUses(code)); + } + boolean unused = n.isUnused(); + if( unused ) { + assert !(n instanceof StopNode); // StopNodes can die if all code in the compunit dies + n.kill(); // Just plain dead + } + if( obs != null ) obs.after(n, x!=null ? x : unused ? n : null, true); + } + assert !CodeGen.expensiveAssert(0) || schedulableUses(code); + + } + + // 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. + // Pessimistic solver assert + public static boolean progressOnList(CodeGen code, WorkList list ) { + code._midAssert = true; + Node changed = code._stop.walk( n -> { + // Ignore most in-progress things + if( n.iskeep() ) return null; + // Types must be forwards, even if on the worklist. + Type nval = n.compute(); + assert nval.isa(n._type) : "Non-monotonic peep: "+n+"#"+n._nid+" old="+n._type+" new="+nval+" inputs="+inputTypes(n)+" peep="+n; + if( list.on(n) ) + return null; // On worklist is ok! + if( n instanceof CallEndNode cend ) { + if( code._iter._workInline.on(cend) ) + return null; // On inline worklist is ok! + assert cend.maybeInline() <= 0 : "Inline fired and not on worklist, CallEndNode#"+cend._nid; + } + // Off list: must be live, in GVN table, and no peeps. + assert n.nOuts() > 0 : "Unused live node: "+n; + assert code._gvn.get(n)==n : "Not in GVN table: "+n; + Node m = n.peepholeOpt(); + assert m==null : "Peep fired and not on worklist, "+n.getClass().getSimpleName()+"#"+n._nid+" -> "+m; + return m; + }); + code._midAssert = false; + return changed==null; + } + + private static String inputTypes(Node n) { + StringBuilder sb = new StringBuilder("["); + for( Node in : n._inputs ) + sb.append(in==null ? "null" : in.getClass().getSimpleName()+"#"+in._nid+"="+in+":"+in._type+":keep="+in.iskeep()).append(','); + return sb.append(']').toString(); + } + + // GCM assumes every movable node can be placed no earlier than its inputs + // and no later than the LCA of its uses. Catch graphs where a use escapes + // above an input-defined branch before the later scheduling pass walks off + // the top of the idom tree. + private static final IdentityHashMap earlyCache = new IdentityHashMap<>(); + private static final BitSet VISIT = new BitSet(); + public static boolean schedulableUses(CodeGen code) { + earlyCache.clear(); + Node bad = code._stop.walk( n -> { + if( n.iskeep() || n.isDead() || n.isConst() || + n instanceof CFGNode || n instanceof PhiNode || n instanceof ProjNode ) + return null; + assert VISIT.isEmpty(); + CFGNode early = CFGNode.earlyCFG(n,earlyCache,VISIT); + VISIT.clear(); + if( early == null ) return null; + CFGNode lca = null; + for( Node use : n._outputs ) { + CFGNode ublk = useBlock(n,use); + if( ublk != null ) + lca = ublk.domLCA(lca,null); + } + if( lca == null ) return null; + if( !early.sameFun(lca) ) return null; + assert early.dominates(lca,null) : badSchedule(n,early,lca); + return null; + }); + return bad == null; + } + + private static CFGNode useBlock(Node n, Node use) { + if( use == null ) + return null; + if( use instanceof PhiNode phi ) { + CFGNode found = null; + for( int i=1; i= phi.region().nIns() ) + return null; + found = phi.region().cfg(i).domLCA(found,null); + } + return found; + } + return CFGNode.safeCFG(use); + } + + private static String badSchedule(Node n, CFGNode early, CFGNode lca) { + StringBuilder sb = new StringBuilder("Unschedulable data node ") + .append(n.getClass().getSimpleName()).append('#').append(n._nid) + .append(" early=").append(cfg(early)) + .append(" use-lca=").append(cfg(lca)) + .append(" node=").append(n).append("\ninputs:"); + for( int i=0; i { + + 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; + + public 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( Ary ary ) { + for( E n : ary ) + push(n); + } + public void addAll( E[] es ) { + for( E n : es ) + push(n); + } + + + /** + * True if Node is on the WorkList + */ + public boolean on( E x ) { return _on.get(x._nid); } + + /** + * Removes a random Node from the WorkList; null if WorkList is empty + */ + public 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 boolean isEmpty() { return _len==0; } + //Node[] asAry() { return Arrays.copyOf(_es,_len); } + //public void clear() { + // _len = 0; + // _on.clear(); + // _R.setSeed(_seed); + // _totalWork = 0; + //} + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/Parser.java b/chapter-modules/src/main/java/com/seaofnodes/simple/Parser.java new file mode 100644 index 00000000..031d36c6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/Parser.java @@ -0,0 +1,3157 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.node.ScopeNode.Kind; + +import static com.seaofnodes.simple.util.Utils.TODO; + +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 { + + // Compile driver + public final CodeGen _code; + + // The Lexer. Thin wrapper over a byte[] buffer with a cursor. + private Lexer _lexer; + + // Class prefix string; TODO: use something short: "%" + public static final String CLZ = "class:"; + private static final String clzPrefix = CLZ; + public static String addClzPrefix( String x ) { + assert !x.startsWith(clzPrefix); + return (clzPrefix+x).intern(); + } + public static boolean startsClzPrefix( String s ) { + return s.startsWith(clzPrefix); + } + public static boolean isPublicClassName( String name ) { + if( startsClzPrefix(name) ) name = name.substring(clzPrefix.length()); + return !name.isEmpty() && name.charAt(0)!='_' && name.indexOf("._")==-1; + } + + // Source file for compilation + public CompUnit _ref; + + // Current classname being parsed + private String _nestedType; + /** + * 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 + */ + public static final HashSet KEYWORDS = new HashSet<>(){{ + add("bool"); + add("break"); + add("byte"); + add("continue"); + add("else"); + add("f32"); + add("f64"); + add("false"); + add("flt"); + add("i16"); + add("i32"); + add("i64"); + add("i8"); + add("if"); + add("int"); + add("new"); + add("null"); + add("return"); + add("struct"); + add("true"); + add("u1"); + add("u16"); + add("u32"); + add("u8"); + 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 _breakScope; // Merge all the while-breaks here + ScopeNode _continueScope; // Merge all the while-continues here + ScopeNode _returnScope; // Merge all the function exits here + Ary _frefs; // Forward refs created during this parse + TypeStruct _ctorOpenStruct; // Open struct being initialized by an allocation constructor block. + // Constructor exit types survive scope teardown until all fields are known. + // A null entry denotes a constructor with no returning path. + private ArrayList> _ctorExits; + + // Mapping from a type name to a Type. The string name matches + // `type.str()` call. + public static final HashMap TYPES = new HashMap<>(); + public static final HashMap INIT_TYPES = + new HashMap<>() {{ + put("bool",TypeInteger.U1 ); + put("byte",TypeInteger.U8 ); + put("f32" ,TypeFloat .F32); + put("f64" ,TypeFloat .F64); + put("flt" ,TypeFloat .F64); + put("i16" ,TypeInteger.I16); + put("i32" ,TypeInteger.I32); + put("i64" ,TypeInteger.BOT); + put("i8" ,TypeInteger.I8 ); + put("int" ,TypeInteger.BOT); + put("u1" ,TypeInteger.U1 ); + put("u16" ,TypeInteger.U16); + put("u32" ,TypeInteger.U32); + put("u8" ,TypeInteger.U8 ); + put("val" ,Type.TOP); // Marker type, indicates type inference + put("var" ,Type.BOTTOM); // Marker type, indicates type inference + }}; + + + public Parser(CodeGen code ) { + _code = code; + } + + @Override + public String toString() { return _lexer==null ? "" : _lexer.toString(); } + + // Debugging utility to find a Node by index + public Node f(int nid) { return _code.f(nid); } + + // Read and set CTRL + private Node ctrl() { return _scope.ctrl(); } + private N ctrl(N n) { return _scope.ctrl(n); } + // Read and set Memory + private Node mem() { return _scope.mem(1); } + private void mem(Node mem) { _scope.mem(mem); } + // Structural label for precise memory Phis. The '$' prefix cannot be + // referenced in Simple source. + public static String memName(int alias) { return ("$"+alias).intern(); } + + // Ordinary parsing carries one whole-memory value. Alias partitioning is + // graph optimization, not parser state. + private void storeMem( Node st, int alias, Node prior ) { + mem(alias==1 ? st : mergeAlias(prior,alias,st)); + } + + // A semantic graph partition representing the complete memory after a + // precise alias update. ScopeNode treats this as one opaque value. + private Node mergeAlias(Node bulk, int alias, Node precise) { + assert bulk != null : "Missing bulk memory for alias #"+alias; + MemMergeNode merge = new MemMergeNode(null,bulk); + merge.alias(alias,precise); + return peep(merge); + } + + + public Ary parse( CompUnit ref ) { + assert _scope == null && _breakScope == null && _continueScope == null && _returnScope == null; + assert _frefs == null; + _lexer = new Lexer(ref); + _frefs = new Ary<>(FRefNode.class); + // Starting Scope has control, memory, initial arguments + _scope = new ScopeNode(); + _scope.define(ScopeNode.CTRL, Type.CONTROL , false, null, _lexer); + _scope.define(ScopeNode.MEM0, TypeMem.BOT , false, null, _lexer); + // Track active scopes for Graph display + _xScopes.push(_scope); + ctrl(_code.XCTRL); + mem(con(TypeMem.BOT)); + + // File-level struct declaration + _ref = ref; + _nestedType = ref._cname; + String typeName = addClzPrefix(ref._cname); + + ReturnNode clzret = parseStruct( true, typeName ); + + if( !_lexer.isEOF() ) throw _errorSyntax("unexpected"); + + // All still-unresolved forward refs + // should immediately resolve to a file-based name which is a Simple + // class type. + Ary frefs = new Ary<>(FRefNode.class); + for( FRefNode fref : _frefs ) { + if( fref.nIns() != 1 && fref._con == FRefNode.FREF_TYPE ) continue; + CompUnit cu = ParseAll.findCompUnitOrThrow(_code,_ref,fref._name); + fref._con = TypeMemPtr.make((byte)2,TypeStruct.make(addClzPrefix(cu._cname),true),true); + frefs.push(fref); + } + + // The class being published + ref._clz = (TypeStruct)TYPES.get(typeName); + + // Clean up and reset + _xScopes.pop(); + _scope.kill(); + _scope = null; + _frefs = null; + + return frefs; + } + + + /** + * Parses a function body, assuming the header is parsed. + */ + private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, String[] ids) { + return parseFunctionBody(sig, loc, null, ids); + } + + private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String[] ids) { + // Record & restore the existing scope vars. The function parse might + // update fields, so when the parse is done reset back to the pre-parse + // state. + Node[] preParse = _scope.save(); + + // Parse function body normally + ReturnNode ret = _parseFunctionBody(null, sig,loc,finals,ids); + + // Reset all fields to pre-parse days + _scope.restore(preParse); + return ret; + } + + + // Parse a function body + + // Stack the {break,continue,return} scopes and a new lexical scope. + // Build a FunNode and parameters, extending scope by parm names. + // Parse the function body + // - while( not end-of-function ) + // - - parseStatement + // - - If isInit, also incrementally extend the self-type. + // If isInit + // - upgrade and close the self-type + // - Gather fields and store values + // - returns self, not last + // Unstack the {break,continue,return} scopes. + + private ReturnNode _parseFunctionBody( String funName, TypeFunPtr sig, Lexer loc, String[] ids) { + return _parseFunctionBody(funName, sig, loc, null, ids); + } + + private ReturnNode _parseFunctionBody( String funName, TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String[] ids) { + Constructor outerCtor=_pendingCtor; + _pendingCtor=null; + boolean isInit = FunNode.isInit(funName); + boolean isCtor = isInit || isExplicitConstructorName(funName); + String typeName = isCtor ? ((TypeMemPtr)sig._sig[0])._obj._name : null; + + // Stack parser state on the local Java stack, and unstack it later + _scope.push(new Kind.Func(funName,isCtor && !funName.endsWith("."))); + ScopeNode breakScope = _breakScope; _breakScope = null; + ScopeNode continueScope = _continueScope; _continueScope = null; + ScopeNode returnScope = _returnScope; _returnScope = null; + + int oldUID = _code.UID(); // Used to approximate function size + FunNode fun = (FunNode)peep(new FunNode(loc(),sig, funName, _ref, null, _ref._start)); + // Once the function header is available, install in linker table - + // allowing recursive functions. Linker matches on declared args and + // exact fidx, and ignores the return (because the fidx will only match + // the exact single function). + _code.link(fun); + + Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole(); + + // Pre-call the function from Start, with worse-case arguments. This + // represents all the future, yet-to-be-parsed functions calls and + // external calls. + _scope.ctrl(fun); // Scope control from function + // Private mem alias tracking per function + Node defaultMem = new ProjNode(_ref._start,1,ScopeNode.MEM0).peephole(); + Node privMem = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,defaultMem).peephole(); + mem(privMem); + // All args, "as-if" called externally + for( int i=0; i and , local vars are really struct fields and + // get exposed as discovered. This allows late local functions to + // use early declared fields. + if( isInit ) + nvar = updateSelfAsFieldsDiscovered( typeName, nvar ); + } + + if( isInit ) finishConstructor(); + _pendingCtor=outerCtor; + + ParmNode self = fun.parm(2); + // Last expression is the return + if( ctrl()._type==Type.CONTROL ) { + // Constructors get special return types + if( fun.isClz() ) { + assert last!=null; + } else if( isCtor ) { + last = fun.parm(3); + } + addReturn(last); + // exit path is now dead (nothing live after a "return") + } + if( isCtor ) + upgradeSelfTypeAndStoreFields( typeName, fun.isClz(), self, fun.parm(3), isInit ); + + // Build a return from the _returnScope. + // Can be no returns for never-exit functions + Node rctl = _returnScope==null ? _code.XCTRL : _returnScope.ctrl().peephole(); + Node rmem = rctl==_code.XCTRL ? con(TypeMem.TOP) : _returnScope.mem(); + Node expr = rctl==_code.XCTRL ? con(Type.TOP) : _returnScope._inputs.last().peephole(); + ReturnNode ret = (ReturnNode)peep(new ReturnNode(rctl, rmem, expr, rpc, fun)); + fun.setRet(ret); + // Approximate function size, for inlining heuristics + fun._approxUIDs = _code.UID() - oldUID; + for( Node cend : ret.outs() ) + if( cend instanceof CallEndNode ) + throw TODO(); //_code.add(cend); // have call ends ALREADY? just barely made the Return + + // Export (and keep-alive) functions until prove dead + _ref.addFun(_code,fun); + + // Unstack parser state + if( _returnScope != null ) _returnScope.kill(); + _returnScope = returnScope; + _continueScope = continueScope; + _breakScope = breakScope; + _scope.pop(); + + return ret; + } + + private int updateSelfAsFieldsDiscovered( String typeName, int nvar ) { + int lex = _scope.klast()._lexSize + 2/*Skip self and selfMem*/; + // Load self type in and out of the TYPES hashtable. + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + // Start looking for newly declared vars at the lexical scope start, + // skipping two ("self" and "selfmem") and fields seen already. + while( lex + nvar < _scope.nIns() ) { + Var v = _scope.var(lex + nvar++); + // Field with sharper type + Field old = tself.field(v._name); + Field fld = v._extern!=0 + ? Field.make(v._name,v.type(),v._extern,false,true) + : old == null + ? Field.make(v._name,v.type(),_code.alias(_ref._cname),v._final) + : old.makeFrom(v.type()); + tself = tself.addOrUpdate(fld); + } + TYPES.put(typeName,tself); + return nvar; + } + + + void upgradeSelfTypeAndStoreFields( String typeName, boolean isClz, ParmNode self, ParmNode smem, boolean upgrade ) { + int lex = _scope.klast()._lexSize; + assert _scope.var(lex)._name=="self"; + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + int base = lex+2; // Skip ctrl, mem + if( upgrade ) { + // Default / defines the struct and can sharpen fields. + // A named constructor only initializes the already-closed struct. + for( int i=0; i receives the + // broad allocation pointer, while private memory tracks initialized fields. + self._declaredType = self._type = isClz ? TypeMemPtr.make((byte)2,tself,true) : TypeMemPtr.make(constructorRecv(tself)); + FunNode fun = self.fun(); + fun.setSig(fun.sig().makeFrom(self._type,0)); + if( !isClz ) // returns upgraded private memory + smem._declaredType = smem._type = TypeMem.makePrivate(tself); + } + + if( !upgrade ) { + IdentityHashMap exit = null; + if( _returnScope!=null && !_returnScope.ctrl()._type.isHigh() ) { + exit = new IdentityHashMap<>(); + int first = _scope._kinds.at(_scope.depth()-2)._lexSize+2; + for( int i=first; i updates the private memory that got + // passed in - is treated like a normal argument and not like memory. + Node mmm = isClz ? _returnScope.mem(1) : smem; + + + // Store constructor results into fields + for( int i=0; i return memory. + _returnScope.mem(mmm); + //_code.add(mmm); + } + + // Validate each user constructor, or an implicit empty one, before the + // declaration scope goes away. Later fields use their declaration defaults. + private static boolean initialized(Type t) { + return t!=Type.BOTTOM && !t.isHigh() && (!(t instanceof TypeNil tn) || tn.notNull()); + } + + private void checkInitFields(TypeStruct ts, int base) { + if( _returnScope==null || _returnScope.ctrl()._type.isHigh() ) return; + boolean implicit = _ctorExits.isEmpty(); + if( implicit ) _ctorExits.add(new IdentityHashMap<>()); + for( IdentityHashMap exit : _ctorExits ) { + if( exit==null ) continue; + for( Field fld : ts._fields ) { + int idx = fieldVarIdx(base,fld._fname); + if( idx == -1 ) continue; + Var v = _scope.var(idx); + if( !v._uninit || v._fref ) continue; + Type t = exit.getOrDefault(v,_returnScope.in(idx)._type); + if( !initialized(t) ) + throw error("'"+ts._name+"' is not fully initialized, field '"+fld._fname+ + (implicit ? "' needs to be set in a constructor" : + "' is only partially set in the constructor"),v._loc); + } + } + } + + private int fieldVarIdx( int base, String name ) { + for( int i=base; i<_scope.nIns(); i++ ) + if( _scope.var(i)._name == name ) + return i; + return -1; + } + + // Named constructors update field Vars from the enclosing initializer + // scope. Constructor arguments and locals begin at limit and must not + // shadow the field state gathered into private memory. + private int fieldVarIdxBefore( int limit, String name ) { + for( int i=limit-1; i>=0; i-- ) + if( _scope.var(i)._name == name ) + return i; + return -1; + } + + private boolean isConstructorDecl(String name) { + TypeStruct self = _scope.constructorSelf(); + return self != null && name == initFieldName(self._name); + } + + private boolean inExplicitConstructor() { + Kind.Func func = (Kind.Func)_scope._kinds.at(_scope.enclosingFuncOrDecl()); + return isExplicitConstructorName(func._name); + } + + private boolean isExplicitConstructorName(String funName) { + TypeStruct self = _scope.constructorSelf(); + return self != null && funName == (self._name+"."+initFieldName(self._name)).intern(); + } + + private static TypeStruct constructorRecv(TypeStruct self) { + Field[] fs = new Field[self._fields.length]; + for( int i=0; i + * '{' statements '}' + * + * Does not parse the opening or closing '{}' + * @return a {@link Node} or {@code null} + */ + private Node parseBlock(Kind kind) { + // Enter a new scope + _scope.push(kind); + Node last = _code.ZERO; + while (!peek('}') && !_lexer.isEOF()) { + _code.add(last); + last = parseStatement(); + } + // Exit scope + last.keep(); + _scope.pop(); + return last.unkeep(); + } + + /** + * Parses a statement + * + *

+     *     returnStatement | declStatement | blockStatement | ifStatement | expressionStatement
+     * 
+ * @return a {@link Node} or {@code null} + */ + private Node parseStatement() { + if( matchx("return") ) return parseReturn(); + else if (matchx("if") ) return parseIf(); + else if (matchx("while") ) return parseWhile(); + else if (matchx("for") ) return parseFor(); + else if (matchx("break") ) return parseBreak(); + else if (matchx("continue")) return parseContinue(); + else if (matchx("struct") ) return parseStruct(); + else if (inStructDeclaration() && matchx("new")) return parseConstructorDeclaration(); + else if (matchx(";") ) return _code.ZERO; // Empty statement + // Break ambiguity around leading function types and starting a block + else if (peek('{') && !isTypeFun() ) { + match("{"); + return require(parseBlock(new Kind.Block()),"}"); + } + // Declaration or normal assignment/expression + else return parseDeclarationStatement(); + } + + private boolean inStructDeclaration() { + return _scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name); + } + + // Constructor declaration: new TypeName = { args -> body }; + private record Constructor(String name, Lexer body) {} + private Constructor _pendingCtor; + + private Node parseConstructorDeclaration() { + TypeStruct self = _scope.constructorSelf(); + assert self != null; + String name = requireId(); + String expect = initFieldName(self._name); + if( name != expect ) + throw error("Constructor name '" + name + "' must match type name '" + expect + "'"); + require("="); + if( _pendingCtor!=null ) throw error("Redefining constructor '"+name+"'"); + _pendingCtor=new Constructor(name,loc()); + // Parse the body after all field declarations are known, so self.x + // and x share a local slot even when x is declared below this body. + require("{"); + int depth=1; + while( depth>0 ) { + _lexer.skipWhiteSpace(); + if( _lexer.isEOF() ) throw error("Unclosed constructor body"); + char ch=_lexer.nextChar(); + if( ch=='{' ) depth++; + else if( ch=='}' ) depth--; + else if( ch=='"' || ch=='\'' ) { + char quote=ch; + while( !_lexer.isEOF() ) { + ch=_lexer.nextChar(); + if( ch=='\\' ) _lexer.nextChar(); + else if( ch==quote ) break; + } + } + } + require(";"); + return _code.ZERO; + } + + private void finishConstructor() { + if( _pendingCtor==null ) return; + Constructor ctor=_pendingCtor; + _pendingCtor=null; + Lexer after=_lexer; + _lexer=new Lexer(ctor.body()); + defineConstructor(ctor.name()); + _lexer=after; + } + + private void defineConstructor(String name) { + TypeStruct self=_scope.constructorSelf(); + Node ctor = constructorFunc(name); + TypeFunPtr sig = (TypeFunPtr)ctor._type; + String clzName = addClzPrefix(self._name); + TypeStruct clz = (TypeStruct)TYPES.get(clzName); + Field old = clz==null ? null : clz.field(""); + if( old != null ) + throw error("Redefining constructor '" + name + "'"); + Field fld = Field.make("",sig,_code.alias(_ref._cname),true); + TYPES.put(clzName,clz==null ? TypeStruct.make(clzName,true,fld) : clz.add(fld)); + require(ctor,";"); + } + + /** + * Parses a while statement + * + *
+     *     while ( expression ) statement
+     * 
+ * @return a {@link Node}, never {@code null} + */ + private Node parseWhile() { + require("("); + return parseLooping(false); + } + + + /** + * Parses a for statement + * + *
+     *     for( var x=init; test; incr ) body
+     * 
+ * @return a {@link Node}, never {@code null} + */ + private Node parseFor() { + // { var x=init,y=init,...; + // while( test ) { + // body; + // next; + // } + // } + require("("); + _scope.push(new Kind.Block()); // Scope for the index variables + if( !match(";") ) // Can be empty init "for(;test;next) body" + parseDeclarationStatement(); // Non-empty init + Node rez = parseLooping(true); + _scope.pop(); // Exit index variable scope + return rez; + } + + // Shared by `for` and `while` + private Node parseLooping( boolean doFor ) { + + var savedContinueScope = _continueScope; + var savedBreakScope = _breakScope; + + // 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(loc(),null,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 = peek(';') ? con(1) : parseAsgn(); + require( doFor ? ";" : ")" ); + + // IfNode takes current control and predicate + Node ifNode = new IfNode(ctrl(), pred.keep()).peephole(); + // Setup projection nodes + CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + CFGNode ifF = (CFGNode)new CProjNode(ifNode.unkeep(), 1, "False").peephole(); + + // for( ;;next ) body + int nextPos = -1, nextEnd = -1; + if( doFor ) { + // Skip the next expression and parse it later + nextPos = pos(); + skipAsgn(); + nextEnd = pos(); + require(")"); + } + + // 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()); + _breakScope.addGuards(ifF,pred,true); // Up-cast predicate + + // 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 + _scope.addGuards(ifT,pred.unkeep(),false); // Up-cast predicate + parseStatement().isKill(); // Parse loop body + _scope.removeGuards(ifT); + + // Merge the loop bottom into other continue statements + if (_continueScope != null) { + _continueScope = jumpTo(_continueScope); + _scope.kill(); + _scope = _continueScope; + } + + // Now append the next code onto the body code + if( doFor ) { + int old = pos(nextPos); + if( !peek(')') ) + parseAsgn(); + if( pos() != nextEnd ) + throw errorSyntax( "Unexpected code after expression" ); + pos(old); + } + + // 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.balanceLoopFRefs(_scope); + exit.balanceLoopFRefs(_scope); + 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. + // During sys parsing, there is no xscope here. + if( !_xScopes.isEmpty() ) { + _xScopes.pop(); + _xScopes.push( exit ); + } + _scope = exit; + return _code.ZERO; + } + + private ScopeNode jumpTo(ScopeNode toScope) { + ScopeNode cur = _scope.dup(); + ctrl(_code.XCTRL); // 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.depth() > _breakScope.depth() ) + 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.depth() <= _breakScope.depth(); + toScope.ctrl(toScope.mergeScopes(cur,loc()).peephole()); + return toScope; + } + + private void checkLoopActive() { if (_breakScope == null) throw error("No active loop for a break or continue"); } + + private Node parseContinue() { checkLoopActive(); _continueScope = require(jumpTo( _continueScope ),";"); return _code.ZERO; } + private Node parseBreak () { + checkLoopActive(); + // At the time of the break, and loop-exit conditions are only valid if + // they are ALSO valid at the break. It is the intersection of + // conditions here, not the union. + _breakScope.removeGuards(_breakScope.ctrl()); + _breakScope = require(jumpTo(_breakScope ),";"); + _breakScope.addGuards((CFGNode)_breakScope.ctrl(), null, false); + return _code.ZERO; + } + + // Look for an unbalanced `)`, skipping balanced + private void skipAsgn() { + int paren=0; + while( true ) + // Next X char handles skipping complex comments + switch( _lexer.nextXChar() ) { + case Character.MAX_VALUE: + throw TODO(); + case ')': + if( --paren<0 ) { + posT( pos() - 1 ); + return; // Leave the `)` behind + } + break; + case '(': paren ++; break; + default: break; + } + } + + + /** + * Parses a statement + * + *
+     *     if ( expression ) statement [else statement]
+     * 
+ * @return a {@link Node}, never {@code null} + */ + private Node parseIf() { + // Parse predicate + require("("); + var pred = require(parseAsgn(), ")"); + return parseTrinary(pred,"else"); + } + + // Parse a conditional expression, merging results. + private Node parseTrinary( Node pred, String fside ) { + // IfNode takes current control and predicate + pred.keep(); + Node ifNode = new IfNode(ctrl(), pred).peephole(); + // Setup projection nodes + CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + CFGNode ifF = new CProjNode(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 + 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.addGuards(ifT,pred,false); // Up-cast predicate + // Parse true-side flavor + Node lhs = switch( fside ) { + case "else" -> parseStatement(); // if( pred ) stmts; + case ":" -> parseAsgn(); // pred ? asgn; + case "&&" -> parseLogical(); // pred && expr + case "||" -> pred; // pred || expr_is_ignored + default -> throw TODO(); + }; + lhs.keep(); + _scope.removeGuards(ifT); + + // See if a one-sided def was made: "if(pred) int x = 1;" and throw. + // See if any forward-refs were made, and copy them to the other side: + // "pred ? n*fact(n-1) : 1" + fScope.balanceIf(_scope); + + 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 + // Up-cast predicate, even if not else clause, because predicate can + // remain true if the true clause exits: `if( !ptr ) return 0; return ptr.fld;` + _scope.addGuards(ifF,pred,true); + // Parse false-side flavor + boolean doRHS = false; // RHS is optional for if/else and trinary + Node rhs = switch( fside ) { + case "else" -> (doRHS=match(fside)) ? parseStatement() : con(lhs._type.makeZero()); + case ":" -> (doRHS=match(fside)) ? parseAsgn() : con(lhs._type.makeZero()); + case "&&" -> con(lhs._type.makeZero()); // pred && expr_is_ignored + case "||" -> parseLogical(); // pred || expr + default -> throw TODO(); + }; + rhs.keep(); + _scope.removeGuards(ifF); + // The RHS of `||` is parsed on the false side, so retain its updated + // control, memory and locals just as an explicit else arm does. + if( doRHS || fside.equals("||") ) + fScope = _scope; + pred.unkeep(); + + // Check the trinary widening int/flt + if( !fside.equals("else") ) { + rhs = widenInt( rhs.unkeep(), lhs._type ).keep(); + lhs = widenInt( lhs.unkeep(), rhs._type ).keep(); + } + + _scope = tScope; + _xScopes.pop(); // Discard pushed from graph display + // See if a one-sided def was made: "if(pred) int x = 1;" and throw. + // See if any forward-refs were made, and copy them to the other side: + // "pred ? n*fact(n-1) : 1" + tScope.balanceIf(fScope); + + // Merge results + RegionNode r = ctrl(tScope.mergeScopes(fScope,loc())); + Node ret = peep(new PhiNode("",r,lhs.unkeep(),rhs.unkeep())); + // Immediately fail e.g. `arg ? 7 : ptr` + ParseException err; + if( !fside.equals("else") && (err=ret.err()) !=null ) throw err; + ret.keep(); + r.peephole(); + return ret.unkeep(); + } + + /** + * 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(parseAsgn(), ";"); + return addReturn(expr); + } + private Node addReturn(Node expr) { + expr.keep(); + // Need default memory, since it can be lazy, need to force + // a non-lazy Phi + mem(); // Force a pending lazy bulk-memory Phi + // For and - all fields in scope will be live-on-exit + // and stored into `self` and need to have lazy-phi's inserted. + int lexN = _scope.enclosingFuncOrDecl(); + Kind.Func k = (Kind.Func)_scope._kinds.at(lexN); + if( FunNode.isInit(k._name) ) { + int nestedLexSize = lexN+1 < _scope.depth() ? _scope._kinds.at(lexN+1)._lexSize : _scope.nIns(); + for( int i=k._lexSize; i and - ALL FIELDS IN LAST SCOPE are live + // and need to have lazy-phi's inserted. + + // And fields might not match.... will need matching Var/define with default values + int rlen = _returnScope.nIns()-1; + RegionNode r = ctrl(new RegionNode(null, null,_returnScope.ctrl(), _scope.ctrl()).init().keep()); + _returnScope._merge(_scope, r, rlen); + Node oldExpr = _returnScope.in(rlen); + _returnScope.setDef(rlen,new PhiNode("$expr", r, oldExpr, expr).peephole()); + _code.add(r); + _returnScope.ctrl(r.unkeep()); + } + ctrl(_code.XCTRL); // Kill control + return expr.unkeep(); + } + + /** Parse: [name '='] expr + */ + private Node parseAsgn() { + int old = pos(); + String name = _lexer.matchId(); + // Just a plain expression, no assignment. + // Distinguish `var==expr` from `var=expr` + if( name==null || KEYWORDS.contains(name) || !matchOpx('=','=') ) + { pos(old); return parseExpression(); } + + // Find variable to update + Var def = _scope.lookup(name); + if( def==null ) + def = defineOpenConstructorField(name); + if( def==null ) + throw error("Undefined name '" + name + "'"); + + if( def._extern!=0 ) // Parse the C binding as a memory lvalue + { pos(old); return parseExpression(); } + + // TOP fields are for late-initialized fields; these have never + // been written to, and this must be the final write. Other writes + // outside the constructor need to check the final bit. + if( _scope.in(def._idx)._type!=Type.TOP && def._final && + // Inside an allocation, final assign is OK, outside nope. + // The alloc() call added the allocation scope + !_scope.canInit(def) ) + throw error("Cannot reassign final '"+name+"'"); + checkSharedFunction(def); + + // Parse assignment expression + Node expr = parseAsgn(); + + // Lift expression, based on type + Type decl = def.type(); // Declared type + Node lift = convertExpr(expr.keep(), decl).keep(); + // Update + _scope.update(name,lift); + + // Check for storing into an instance field instead of a local var + int kx = _scope.kindx(def); // Declaration scope + int fx = _scope.enclosingFunction(); // Enclosing function scope + Kind kk = _scope._kinds.at(kx); + Kind fk = _scope._kinds.at(fx); + // Access instance field from 'self' + if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) && + fk instanceof Kind.Func method && !inExplicitConstructor() ) { + // Named field offset on an unknown struct type + TypeMemPtr self = (TypeMemPtr)_scope.var(fk._lexSize).type(); + TypeStruct sobj = (TypeStruct)TYPES.get(self._obj._name); + Field fld = sobj.field(name); + int alias = fld._alias; + Node ptr = _scope.in(method._lexSize); + Node off = fldoff(ptr,name); + Node prior = mem().keep(); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), prior, ptr, off, lift, inExplicitConstructor()).peephole().keep(); + storeMem(st,alias,prior); + st.unkeep(); + prior.unkeep(); + } + lift.unkill(); + + + // Return un-lifted expr + return expr.unkeep(); + } + + // Widen ints to floats; narrow wide int types. + // Early error if types do not match variable. + private Node convertExpr( Node expr, Type t ) { + if( !expr._type.accessISA(t) ) + throw error("Type "+expr._type.str()+" is not of declared type "+t.str()); + if( t instanceof TypeMemPtr tmp && tmp.isFinal() ) + expr = peep(new ReadOnlyNode(expr)); + // A FunPtr carries the semantic edge to its function's Return. Do not + // hide that edge behind a Convert which can fold to a plain constant. + if( expr instanceof FunPtrNode && t instanceof TypeFunPtr ) + return expr; + // Auto-widen array to i64 (cast ptr to raw int bits) + if( t == TypeInteger.BOT && expr._type instanceof TypeMemPtr tmp && tmp._obj.isAry() ) + return peep(new AddNode(peep(new PtrToIntNode(expr)),off(tmp._obj,"[]"))); + // Widen ints to floats, narrow wide types + return peep(new ConvertNode(t,expr)); + } + + // Inferred `var` declarations describe writable slots, not the exact + // value used to initialize them. Explicitly typed locals retain the + // user's nullability and numeric-width guarantees; `val` retains the + // initializer's precise type. + private static Type mutableInferredType(Type t) { + return switch( t ) { + case TypeInteger ti -> TypeInteger.BOT; + case TypeFloat tf -> TypeFloat.F64; + case TypeMemPtr tmp -> tmp.makeVar(); + case TypeFunPtr tfp -> tfp; + case TypeConAry a -> a; + case TypePtr ptr -> TypePtr.PTR; + case TypeScalar ts -> TypeScalar.BOT; + default -> t; + }; + } + + private Type constructorFieldType(Type t) { + if( t instanceof TypeMemPtr ptr && ptr._obj.isConAry() ) { + TypeStruct declared=(TypeStruct)TYPES.get(ptr._obj._name); + TypeStruct shape=typeAry(declared._fields[1]._t,ptr._obj._fields[1]._final); + return TypeMemPtr.make((byte)2,shape,false).withAccess(ptr._ro); + } + return mutableInferredType(t); + } + + // Seed an inferred return merge with the declared numeric family instead + // of a transient constant/range. Pointer nullability remains part of the + // inferred function result and is combined by the later Phi. + private static Type inferredReturnType(Type t) { + return switch( t ) { + case TypeInteger ti -> TypeInteger.BOT; + case TypeFloat tf -> TypeFloat.F64; + default -> t; + }; + } + + private Node widenInt( Node expr, Type t ) { + return (expr._type instanceof TypeInteger || expr._type==Type.NIL) && t instanceof TypeFloat + ? peep(new ToFloatNode(expr)) : expr; + } + + /** + * Parse declaration or expression statement + * declStmt = type var['=' exprAsgn][, var['=' exprAsgn]]* ';' | exprAsgn ';' + *

+ * exprAsgn = var '=' exprAsgn | expr + */ + private Node parseDeclarationStatement() { + int old = pos(); + Type t = type(); + if( peek('.') ) // Ambiguity static vars: "type.var", parse as expression + { pos(old); t=null; } + if( t == null ) + return require(parseAsgn(),";"); + + // now parse var['=' asgnexpr] in a loop + Node n = parseDeclaration(t); + while( match(",") ) + n = parseDeclaration(t); + return require(n,";"); + } + + // The binding modifier sits immediately before the name. It never + // changes permissions on the referenced object. + private boolean bindingFinal(Type t) { + if( match("!") ) return false; + if( match("~") ) return true; + return t==Type.TOP || t instanceof TypeNil; + } + + /** Parse final: [!|~]var['=' asgn] + */ + private Node parseDeclaration(Type t) { + assert t!=null; + // Has var/val instead of a user-declared type + boolean inferType = t==Type.TOP || t==Type.BOTTOM; + boolean xfinal = bindingFinal(t); + Lexer loc = loc(); + String name = requireId(); + // Optional initializing expression follows + boolean fld_final = false; // Field is final, but not deeply final + boolean uninit = false; // Required non-null constructor field + Node expr; + if( match("=") ) { + if( isExternDecl() ) { + if( !(t instanceof TypeFunPtr) ) { + Node ptr = externData(name,t); + // The lexical binding is a fixed address; its contents are mutable. + if( !_scope.define(name,t,true,ptr,loc) ) + throw error("Redefining name '"+name+"'",loc); + _scope.lookup(name)._extern = _code.externAlias(name); + return _code.ZERO; + } + expr = externDecl(name,t); + t = expr._type; // Upgrade declared type to the exact extern decl type + } else if( isConstructorDecl(name) ) { + expr = constructorFunc(name); + } else { + expr = parseAsgn(); + } + // var/val, then type comes from expression + if( inferType ) { + if( expr._type==Type.NIL ) + throw error("a not-null/non-zero expression"); + t = expr._type; + if( !xfinal ) t = mutableInferredType(t); + // Instance fields may change until the constructor finishes. + // Keep the declared family/access, not the default's constant + // value or string contents. Class constants remain static. + else if( _scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name) && !(t instanceof TypeFunPtr) ) + t = constructorFieldType(t); + } + + // expr is a constant function + if( t instanceof TypeFunPtr decl && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { + t = decl.makeFrom(tfp.fidx()); + FunNode fun = _code.link(tfp); + if( fun != null ) + fun.setName(name); // Assign debug name to Simple function + } + + } else { + // Since no initializer, not-final but might need an initializer + + // Need an expression to infer the type. + // Also, if not-null then need an initializing expression. + // Allowed in a class def, because type/init will happen in the constructor. + if( (inferType || (t instanceof TypeNil tn && !tn.nullable() )) && !_scope.inConstructor() ) + throw errorSyntax("=expression"); + // Initial value for uninitialized struct fields. + expr = switch( t ) { + // Required pointers retain their declared shape, but include + // null until every constructor path assigns them. + case TypeNil tn -> tn.nullable() ? con(Type.NIL) : con(t.meet(Type.NIL)); + case TypeInteger ti -> xfinal ? con(Type.BOTTOM) : _code.ZERO; + case TypeFloat tf -> con(xfinal ? Type.BOTTOM : TypeFloat.FZERO); + // Bottom signals type inference: they must be initialized in + // the constructor and that's when we'll discover the type. + case Type tt -> { assert tt==Type.BOTTOM; yield con(tt); } + }; + // Every non-null pointer without a default is constructor-required. + // Without `!` it is also shallow-final; mutability and definite + // initialization are independent properties. + if( (xfinal && !(t instanceof TypeNil)) || (t instanceof TypeNil tn && !tn.nullable()) ) { + uninit = true; + if( xfinal ) fld_final = true; + } + } + + // Lift expression, based on type + // Do not let an authoritative Convert hide the nullable + // not-yet-initialized state from constructor-local type flow. + Node lift = uninit ? expr : convertExpr(expr, t); + + // Define a new name + if( !_scope.define(name,t,xfinal || fld_final,lift, loc) ) + throw error("Redefining name '" + name + "'", loc); + _scope.lookup(name)._uninit = uninit; + return lift; + } + + + /** + * Parse a struct declaration, and return the following statement. + * Structs cannot be redefined, but can be nested. + * + * @return zero + */ + private Node parseStruct( ) { + // keyword "struct" already parsed, so expect the struct name next. + String typeName = requireId(); + require("{"); + + // create type class:NESTED.typeName; no fields. + + String old = _nestedType; + String fullName = _scope.depth()==1 && _nestedType.endsWith(typeName) ? old : (old+"."+typeName).intern(); + _nestedType = fullName; // Recursive structs start with this as their basename + + ReturnNode ret = parseStruct( false, fullName ); + + _nestedType = old; + + // Insert a field into class:NESTED.typeName of "val typeName = fcn-ptr-to-init" + // Function does malloc internally. + // Future self: means subtyping has to deal with "who does the malloc" + String clzName = addClzPrefix(fullName); + TypeFunPtr sig = ret.fun().sig(); + TypeStruct ts = (TypeStruct)TYPES.get(clzName); + String initName = initFieldName(fullName); + Field oldFld = ts==null ? null : ts.field(initName); + Field fld = oldFld==null + ? Field.make(initName,sig,_code.alias(_ref._cname),true) + : oldFld.makeFrom(sig); + Field ctorFld = ts==null ? null : ts.field(""); + // A same-named struct shares its file's class object and flag. + Field flag = ts==null ? null : ts.field("$init"); + ts = ctorFld==null + ? TypeStruct.make(clzName,false,fld) + : TypeStruct.make(clzName,false,fld,ctorFld); + if( flag!=null ) ts=ts.add(flag); + TypeFunPtr alloc = makeAllocator((TypeStruct)TYPES.get(fullName),ret,ctorFld); + ts = ts.add(Field.make("",alloc,_code.alias(_ref._cname),true)); + TYPES.put(clzName,ts); + + // Insert a field in the containing class with the nested class type. + // This is basically sugar for "val CLZNAME = " + // and is a nicer version of updateSelfAsFieldsDiscovered. + _scope.define(typeName, sig, true, new FunPtrNode(sig,_code._start,ret).peephole(), loc()); + + require("}"); + return require(_code.ZERO,";"); + } + + // Build the declaration-side allocation helper. Allocation sites only + // call this function; all layout, initialization and publication choices + // are made here, where the complete struct and constructor are known. + private TypeFunPtr makeAllocator(TypeStruct ts, ReturnNode initRet, Field ctorFld) { + TypeFunPtr ctor = ctorFld==null ? null : (TypeFunPtr)ctorFld._t; + int nargs = ctor==null ? 0 : ctor.nargs()-2; // Hide self and selfMem + Type[] atypes = new Type[nargs]; + for( int i=0; i").intern(); + + int oldUID = _code.UID(); + FunNode fun = (FunNode)new FunNode(loc(),sig,name,_ref,null,_ref._start).peephole(); + _code.link(fun); + Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole(); + Node pub = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun, + new ProjNode(_ref._start,1,ScopeNode.MEM0).peephole()).peephole(); + Node[] parms = new Node[nargs]; + for( int i=0; i(); + // Record & restore global state set during parsing the code + Node oldCtrl= _scope.ctrl().keep(); + Node oldMem = _scope.mem ().keep(); + + // Make the future clazz/instance struct. + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + if( tself==null ) + TYPES.put(typeName, tself = TypeStruct.make(typeName,true)); + else { + assert tself._open; + TYPES.put(typeName,tself = tself.defined()); + } + + // Class object keeps a runtime flag to signal the has run. + if( isClz ) + TYPES.put(typeName,tself=tself.add(Field.make("$init",TypeInteger.BOOL,_code.alias(_ref._cname),false))); + + // Signature of the default constructor + TypeFunPtr sig = structInitSig(isClz,tself); + // Function and argument names of default constructor + String fname = (typeName + (isClz ? "." : ".")).intern(); + String[] ids = new String[]{"self", isClz ? "arg" : "#selfMem"}; + + // Struct decls look like function bodies. Parse function body normally. + ReturnNode ret = _parseFunctionBody(fname,sig,loc(),ids); + _ctorExits = oldExits; + // Unwind global state. + ctrl(oldCtrl.unkeep()); + mem (oldMem .unkeep()); + return ret; + } + + // signature: { self arg/selfMem -> BOT/selfMem } + // Self-memory is the rare *private* (never aliased) memory for the new object. + private TypeFunPtr structInitSig( boolean isClz, TypeStruct tself ) { + // Private uninitialized memory from new object, no escaped values. + TypeMem privMem = isClz ? null : TypeMem.make(1,tself,true,isClz,false,null,null); + Type targ = isClz ? TypeInteger.BOT : privMem; + // Return for private memory has to name all escaped fields, which are not known yet. + // Escape ALL fields, and sharpen later. + Type tret = isClz ? Type.BOTTOM : TypeMem.makePrivate(tself); + Type[] targs = new Type[]{ TypeMemPtr.make((byte)2,tself,isClz), targ }; + // FIDX for FREFs is in the global namespace, needs the correct clz name. + String clzBare = tself._name; + int order = 3; // 3 is always the order, 2nd in the file just behind the + if( isClz ) { + assert startsClzPrefix( clzBare ); + clzBare = clzBare.substring(clzPrefix.length()).intern(); + order = 2; // 2 is always the order, first in file and skipping reserved #1 + } + return TypeFunPtr.make1((byte)2, true, targs, tret, _code.fidx(clzBare,order)); + } + + // A class type carries the constructor function as a required final field. + private TypeStruct classStruct( String typeName, TypeFunPtr init, boolean open ) { + return TypeStruct.make(addClzPrefix(typeName),open,Field.make(initFieldName(typeName),init,_code.alias(_ref._cname),true)); + } + + private static String initFieldName( String typeName ) { + if( startsClzPrefix(typeName) ) + typeName = typeName.substring(clzPrefix.length()); + int idx = typeName.lastIndexOf('.'); + return (idx == -1 ? typeName : typeName.substring(idx+1)).intern(); + } + + private TypeStruct latestStruct( TypeStruct ts ) { + Type t = TYPES.get(ts._name); + return t instanceof TypeStruct ts2 ? ts2 : ts; + } + + private Field fieldOrOpen( TypeStruct ts, String name, boolean xfinal ) { + ts = latestStruct(ts); + Field fld = ts.field(name); + if( fld != null ) return fld; + if( !ts._open || !ts._fref ) return null; + fld = Field.make(name,Type.BOTTOM,_code.alias(_ref._cname),xfinal); + TYPES.put(ts._name,ts = ts.add(fld)); + if( _ctorOpenStruct != null && _ctorOpenStruct._name == ts._name ) + _ctorOpenStruct = ts; + return fld; + } + + private Var defineOpenConstructorField( String name ) { + if( _ctorOpenStruct==null ) return null; + Field fld = fieldOrOpen(_ctorOpenStruct,name,true); + if( fld == null ) return null; + _scope.define(name,fld._t,fld._final,con(Type.TOP),loc()); + return _scope.lookup(name); + } + + + // Parse and return a type or null. Valid types always are followed by an + // 'id' which the caller must parse. This lets us distinguish forward ref + // types (which ARE valid here) from local vars in an (optional) forward + // ref type position. + + // t = int|i8|i16|i32|i64|u8|u16|u32|u64|byte|bool | flt|f32|f64 | val | var | struct[?] + + // Structs have nested names, and can be discovered with a partial name + // match. Example, searching for a type name "C.D" in the namespace "A.B". + // This will match "A.B.C.D", then "A.C.D", then "C.D". + + // Will also match external names like "sys.io" or "externModule.subtype.subtype" + + private Type type() { + // Only type with a leading `{` is a function pointer... + if( peek('{') ) return typeFunPtr(); + + // Otherwise you get a type name + int old1 = pos(); + boolean writable = match("!"); + boolean readonly = !writable && match("~"); + String tname = _lexer.matchId(); + if( tname==null ) return posT(old1); + + // Convert the type name to a type. + Type t0 = TYPES.get(tname), t1 = t0; + // No new types as keywords + if( KEYWORDS.contains(tname) ) { + if( t0 == null ) return posT(old1); + // Something like "int" or "f64" or "var". + // Can be "int[]" so still need to check array-ness + } else { + if( t0 == Type.BOTTOM || t0 == Type.TOP ) return t0; // var/val type inference + + if( t0 != null ) { + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null ) { pos(old2); break; } + tname = sname; + t0 = t; + } + } else { + // Type A.B.C starts with just A. Search our nested type path, + // e.g. path "X.Y.Z", in order: + + // If A==X its now a local type. + // If another dot, repeat for B==Y then C==Z + // Else if AX.B exists, done + // Else AX.B is a local forward ref. + // + // Else its a short cut form, one of: + // X.Y.Z.A + // X.Y.A + // X.A + // Else this all failed, assume a forward ref with A as an external symbol: + // A + + String root = _nestedType; + int ridx = root.indexOf('.'); + if( ridx != -1 ) root = root.substring(0,ridx); + if( tname.equals(root) ) { + boolean localForward = false; + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null && !(_nestedType.equals(tname) || _nestedType.startsWith(tname+".")) && !localForward ) { + pos(old2); + break; + } + tname = sname; + t0 = t; + localForward |= t==null; + } + } else { + String nest = _nestedType, fullq = tname; + while( true ) { + fullq = nest+"."+tname; // Full qualified name + t0 = TYPES.get(fullq); + if( t0 != null ) + { tname = fullq; break; } // Search succeeded + // Subtract a layer from nested typenames + int idx = nest.lastIndexOf('.'); + if( idx== -1 ) break; // Search failed; leave as an external symbol + nest = nest.substring(0,idx); + } + if( t0==null ) { + CompUnit cu = ParseAll.findCompUnit(_code,_ref,tname); + if( cu != null ) { + tname = cu._cname; + t0 = TYPES.get(tname); + } + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + tname = tname+"."+sname; + t0 = TYPES.get(tname); + } + } else { + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null ) { pos(old2); break; } + tname = sname; + t0 = t; + } + } + } + } + + tname = tname.intern(); + // Still no type found? Assume forward reference + t1 = t0 == null + ? TypeStruct.forward(tname) // Null: assume a forward ref type + : t0; + } + + // Structs are always actually references + if( t1 instanceof TypeStruct ts1 ) + t1 = TypeMemPtr.make(ts1); + + // A qualifier before the name applies to the struct reference, not + // to an enclosing array. Arrays have their own [] / [~] qualifier. + if( t1 instanceof TypeMemPtr tmp ) { + if( readonly || !writable ) t1 = tmp.makeRO(); + } else if( writable || readonly ) + return posT(old1); + + // Nest arrays and '?' as needed + Type t2 = t1; + while( true ) { + if( match("?") ) { + if( t2 instanceof TypeMemPtr tmp ) { + if( tmp.nullable() ) throw error("Type "+t2+" already allows null"); + t2 = tmp.makeNullable(); + } else + throw error("Type "+t0+" cannot be null"); + } else if( match("[~]") ) { + t2 = TypeMemPtr.make(typeAry(t2,true)).makeRO(); + } else if( match("[]") ) { + t2 = TypeMemPtr.make(typeAry(t2,false)); + } else + break; + } + + // Check no forward ref + if( t0 != null ) return t2; + // Check valid forward ref, after parsing all the type extra bits. + // Cannot check earlier, because cannot find required 'id' until after "[]?" syntax + int old2 = pos(); + if( !match("!") ) match("~"); + String id = _lexer.matchId(); + if( !(peek(',') || peek(';') || peek('=') || match("->")) || id==null ) + return posT(old1); // Reset lexer to reparse + pos(old2); // Reset lexer to reparse + // Yes a forward ref, so declare it + forwardRefType(tname,(TypeStruct)((TypeMemPtr)t1)._obj); + // Return the (array, final) type + return t2; + } + + private TypeMemPtr forwardRefType( String tname, TypeStruct tself ) { + TYPES.put(tname,tself); + String clzName = addClzPrefix( tname ); + TYPES.put(clzName, classStruct(tname,structInitSig(false,tself),true)); + return TypeMemPtr.make(tself); + } + + private TypeMemPtr forwardRefType( String tname ) { + tname = fullTypeName(tname); + return forwardRefType(tname,TypeStruct.forward(tname)); + } + + private String fullTypeName( String tname ) { + String nest = _nestedType, fullq; + while( true ) { + fullq = nest+"."+tname; + if( TYPES.get(fullq) != null ) + return fullq.intern(); + int idx = nest.lastIndexOf('.'); + if( idx== -1 ) return fullq.intern(); + nest = nest.substring(0,idx); + } + } + + // Make an array type of t. Always record a mutable version, + // but return the requested version. + private TypeStruct typeAry( Type t, boolean efinal ) { + // Non-null reference arrays are valid types, even though an ordinary + // zero-filled `new T[len]` cannot construct one. Allocation retains + // that check in alloc(); trusted runtimes (notably argv construction) + // may fill the private array completely before publishing it. + String tname = ("[]"+t.str()).intern(); + TypeStruct ta = (TypeStruct)TYPES.get(tname); + if( ta==null ) { + // Primitive arrays use the null compunit + String rcname = TYPES.get(t) instanceof TypeStruct ? _ref._cname : ("_."+t).intern(); + ta = TypeStruct.makeAry(tname,TypeInteger.U32,_code.alias(rcname),t,_code.alias(rcname), false ); + TYPES.put(tname,ta); + } + // Already have the aliases, just efinal is wrong + return TypeStruct.makeAry(tname,TypeInteger.U32,ta._fields[0]._alias,t,ta._fields[1]._alias,efinal ); + } + + private Type implicitSelfType() { + TypeStruct self = _scope.constructorSelf(); + // Ordinary instance methods accept the declared object shape. The + // constructor-only receiver starts at the low/default field values, + // and is built explicitly by constructorFunc(). + return self == null ? TypePtr.PTR : TypeMemPtr.make(self); + } + + // A function type is `{ type... -> type }` or `{ type }`. + private Type typeFunPtr() { + int old = pos(); // Record lexer position + match("{"); // Skip already-peeked '{' + Type t0 = type(); // Either return or first arg + if( t0==null ) return posT(old); // Not a function + Ary ts = new Ary<>(Type.class); + // Always reserve a receiver slot. Outside constructor scopes it is + // broad and unused, so call sites can uniformly pass their self. + ts.push(implicitSelfType()); + if( match("}") ) // No-arg function { -> type } + return TypeFunPtr.make(match("?"),ts.asAry(),t0); + ts.push(t0); // First argument + while( true ) { + if( match("->") ) { // End of arguments, parse return + Type ret = type(); + if( ret==null || !match("}") ) + return posT(old); // Not a function + return TypeFunPtr.make(match("?"),ts.asAry(),ret); + } + Type t1 = type(); + if( t1==null ) return posT(old); // Not a function + ts.push(t1); + } + } + + // True if a TypeFunPtr, without advancing parser + private boolean isTypeFun() { + int old = pos(); + if( typeFunPtr()==null ) return false; + pos(old); + return true; + } + + /** + * Parse an expression of the form: + * + *

+     *     expr : bitwise [? expr [: expr]]
+     * 
+ * @return an expression {@link Node}, never {@code null} + */ + private Node parseExpression() { + Node expr = parseLogical(); + return match("?") ? parseTrinary(expr,":") : expr; + } + + /** + * Parse an bitwise expression + * + *
+     *     bitwise : compareExpr (('&' | '|' | '^') compareExpr)*
+     * 
+ * @return a bitwise expression {@link Node}, never {@code null} + */ + + private Node parseLogical() { + Node lhs = parseBitwise(); + while (true) { + if( false ) ; + else if( match("&&") ) lhs = parseTrinary(lhs, "&&"); + else if( match("||") ) lhs = parseTrinary(lhs, "||"); + else break; + } + return lhs; + } + + private Node parseBitwise() { + Node lhs = parseEquality(); + while( true ) { + if( false ) ; + else if( matchOp('&') ) lhs = new AndNode(loc(),lhs,null); + else if( matchOp('|') ) lhs = new OrNode(loc(),lhs,null); + else if( match ("^") ) lhs = new XorNode(loc(),lhs,null); + else break; + lhs.setDef(2,parseEquality()); + lhs = peep(lhs); + } + return lhs; + } + + + /** + * Parse an eq/ne expression + */ + private Node parseEquality() { + Node lhs = parseComparison(); + while( true ) { + boolean eq = false; + if( match("==") ) eq = true; + else if( !match("!=") ) break; + lhs.keep(); + Node rhs = parseComparison(); + lhs = peep(new BoolNode.EQ(lhs.unkeep(),rhs)); + if( !eq ) lhs = peep(new NotNode(lhs)); + } + return lhs; + } + + /** + * Parse an expression of the form: + * + *
+     *     expr : shiftExpr < shiftExpr <= shiftExpr...
+     *     expr : shiftExpr > shiftExpr >= shiftExpr...
+     * 
+ * @return an comparator expression {@link Node}, never {@code null} + */ + private Node parseComparison() { + Node lhs = parseShift(); + int dir = parseCompDir(); + // No compare + if( dir==0 ) return lhs; + lhs.keep(); + + // Compare + Node rhs = parseShift().keep(); + Node cmp = makeCompBool(dir,lhs.unkeep(),rhs).keep(); // Convert to a bool + + // Stacked compares? + int dir0 = parseCompDir(); + if( dir0 == 0 ) { + rhs.unkill(); + return cmp.unkeep(); + } + + // rhs is keeped() and becomes lhs + // cmp is NOT keeped() and is the last test + Node ifNode = new IfNode(ctrl(), cmp.unkeep()).peephole(); + Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole(); + // False side does nothing but capture memory & side-effects + ScopeNode fail = _scope.dup(); + fail.ctrl(ifF); + + // Loop over stacked compares. Each loop moves the old RHS to become + // the new LHS, so e.g. (a < b < c < d) compares (LHS:a < RHS:b) then + // compares (LHS:b < RHS:c) then (LHS:c < RHS:d), etc. + while( dir0 != 0 ) { + if( Math.abs(dir) != Math.abs(dir0) ) + throw error("Mixing relational directions in a chained relational test"); + // True side parses next arm of test + ctrl(ifT.unkeep()); + lhs = rhs; // lhs is keeped() and is old RHS + rhs = parseShift().keep(); + cmp = makeCompBool(dir,lhs.unkeep(),rhs); // Convert to a bool + ifNode = new IfNode(ctrl(), cmp).peephole(); + ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole(); + // Merge result into the fail case + ctrl(ifF); + fail.mergeScopes(_scope.dup(), loc()).peephole(); + // Check next stacked compare + dir0 = parseCompDir(); + } + rhs.unkill(); + ctrl(ifT.unkeep()); + RegionNode r = fail.mergeScopes(_scope, loc()).init(); + _scope = fail; + return new PhiNode("", r,_code.ZERO,con(1)).peephole(); + } + + private int parseCompDir() { + if( match("<=") ) return -1; // Negative for "equals-or-less" + if( match(">=") ) return -2; // 1 for <, 2 for > + if( match("<" ) ) return 1; // Positive for just "less" + if( match(">" ) ) return 2; + return 0; + } + + private Node makeCompBool( int dir0, Node lhs, Node rhs ) { + // Convert to a bool + if( Math.abs(dir0)==2 ) // Swap direction + { Node tmp = lhs; lhs = rhs; rhs = tmp; } + lhs = peep(dir0 < 0 ? new BoolNode.LE(lhs,rhs) : new BoolNode.LT(lhs,rhs)); + return peep(lhs); + } + + /** + * Parse an additive expression + * + *
+     *     shiftExpr : additiveExpr (('<<' | '>>' | '>>>') additiveExpr)*
+     * 
+ * @return a shift expression {@link Node}, never {@code null} + */ + private Node parseShift() { + Node lhs = parseAddition(); + while( true ) { + if( false ) ; + else if( match("<<") ) lhs = new ShlNode(loc(),lhs,null); + else if( match(">>>")) lhs = new ShrNode(loc(),lhs,null); + else if( match(">>") ) lhs = new SarNode(loc(),lhs,null); + else break; + lhs.setDef(2,parseAddition()); + ParseException err; + if( (err=lhs.err()) != null ) + throw err; + lhs = peep(lhs); + } + 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 = peep(lhs); + } + 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 = peep(lhs); + } + return lhs; + } + + /** + * Parse a unary minus expression. + * + *
+     *     unaryExpr : ('-') unaryExpr | '!') unaryExpr | postfixExpr | primaryExpr | '--' Id | '++' Id
+     * 
+ * @return a unary expression {@link Node}, never {@code null} + */ + private Node parseUnary() { + // Pre-dec/pre-inc + int old = pos(); + if( match("--") || match("++") ) { + int delta = _lexer.peek(-1)=='+' ? 1 : -1; // Inc vs Dec + String name = _lexer.matchId(); + if( name!=null ) { + Var n = _scope.lookup(name); + if( n != null && !(n.type() instanceof TypeMemPtr) ) { + if( n._extern!=0 ) + return parsePostfixName(_scope.in(n),name,delta); + if( n._final && !_scope.canInit(n) ) + throw error("Cannot reassign final '"+n._name+"'"); + checkSharedFunction(n); + Node expr = convertExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type()); + _scope.update(n,expr); + return expr; + } + } + // Reset, try again + pos(old); + } + if (match("-")) return peep(new MinusNode(parseUnary())); + if (match("!")) return peep(new NotNode(parseUnary())); + // Else be a primary expression + return parsePrimary(); + } + + /** + * Parse a primary expression: + * + *
+     *     primaryExpr : integerLiteral | "string" | 'char' | true | false | null |
+     *                   new Type | '(' expression ')' | Id['++','--'] |
+     *                   type['.' Id]*
+     * 
+ * @return a primary {@link Node}, never {@code null} + */ + private Node parsePrimary() { + if( _lexer.isNumber(_lexer.peek()) ) return parseLiteral(); + if( _lexer.peek('"') ) return parsePostfix(newString(parseString())); + if( matchx("true" ) ) return con(1); + if( matchx("false") ) return _code.ZERO; + if( matchx("null" ) ) return con(Type.NIL); + if( match ("'" ) ) return parseChar(); + if( match ("(" ) ) return parsePostfix(require(parseAsgn(), ")")); + if( matchx("new" ) ) return parsePostfix(alloc()); + if( match ("{" ) ) return parsePostfix(require(func(),"}")); + + int pos = pos(); + // Expect an identifier now + String id = _lexer.matchId(); + if( id == null || KEYWORDS.contains(id) ) + throw errorSyntax("an identifier or expression"); + int pos2 = pos(); + // Attempt a local var lookup first + Var var = _scope.lookup(id); + if( var==null ) { + // Not a local var, try again as a type name + pos(pos); + Type t = type(); + if( t!=null ) { + // Static typename lookup + if( peek('.') ) // type.fld, which is really a field lookup in the class object + return parsePostfix(con(t)); + // Direct reference to a type. Convert the instance + // type to a class type name + if( !(t instanceof TypeMemPtr tmp) ) { + //return new FRefNode(addClzPrefix(tmp._obj._name), loc()).peephole(); + // Not a local var, but yes a bare type... disallow a local + // FREF and return null, not-a-primary parse. + pos(pos); + return null; + } + } + + // Insert FREF outside enclosing Kind.Func scope. + pos(pos2); + t = FRefNode.FREF_TYPE; + // Check if this is a class. + CompUnit cu = ParseAll.findCompUnit(_code,_ref,id); + if( cu!=null ) { + // Must be a final class-type + String clzName = addClzPrefix(cu._cname); + TypeStruct clz = (TypeStruct)TYPES.get(clzName); + if( clz == null ) + TYPES.put(clzName,clz = TypeStruct.make(clzName,true)); + t = TypeMemPtr.make((byte)2,clz,true); + } + // Define the FRef + var = _scope.defineFRef(id,t,true,loc()); + _frefs.push((FRefNode)_scope.in(var)); + } + + // Load local value + Node rvalue = _scope.in(var); + if( var._extern!=0 ) + return parsePostfixName(rvalue,id,0); + if( var._fref ) + return parsePostfix(rvalue); + // A constructor may read the unfinished object, but each field it + // reads must already be initialized on every path. Read the current + // local value; other fields may still be waiting for their first write. + if( inExplicitConstructor() && var._uninit && missingInit(rvalue,new BitSet()) ) + throw error("Field '"+id+"' might not be initialized",var._loc); + + // Check for an instance field load + int kx = _scope.kindx(var); // Declaration scope + int fx = _scope.enclosingFunction(); // Enclosing function scope + Kind kk = _scope._kinds.at(kx); + Kind fk = _scope._kinds.at(fx); + // Access instance field from 'self' + if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) && fk instanceof Kind.Func method && !inExplicitConstructor() ) + return parsePostfixName(_scope.in(method._lexSize),id,0); + + // Check for a function-escaping variable; these require true + // closures. Final constants are OK; final vars require a hidden var + // argument; not-final fields can just use an explicit struct arg. + if( kx < fx && !inExplicitConstructor() && // Out of scope + !(var._final && rvalue._type.isConstant()) ) { // Not a final constant + if( kx > 0 ) // Global scope is OK, only ever one of these (not one per function invoke) + throw error("Variable '"+var._name+"' is out of function scope and must be a final constant"); + + // var.idx==2 is self argument, 3 is args, 4 or more are now static globals + if( var._idx >= 4 ) { + // Read from global scope. + // Surely a nicer way to get the top-level scope compile name? + Var clzptr = _scope.var( 2 ); + // Get the most recent sharpen global + TypeStruct clz = (TypeStruct) TYPES.get( ((TypeMemPtr) clzptr.type())._obj._name ); + TypeMemPtr clztmp = TypeMemPtr.make( (byte) 2, clz, true ); + return parsePostfixName( con( clztmp ), id,0 ); + //return parsePostfixName(_scope.in(2),id); + } + } + + // Check for assign-update, x += e0; + char ch = _lexer.matchOperAssign(); + if( ch==0 ) { // Normal primary, check for postfix updates + int selfx = _scope.kindFcn(var)._lexSize; + // Might be a method call from inside a method, so no explicit "self". + // Pass the in-scope "self" + return parsePostfixMethod(rvalue,_scope.in(_scope.update(_scope.var(selfx),null))); + } + // Assign-update direct into Scope + if( var._final && !_scope.canInit(var) ) + throw error("Cannot reassign final '"+var._name+"'"); + checkSharedFunction(var); + Node op = opAssign(ch,rvalue, var.type() ); + _scope.update(var,op); + return postfix(ch) ? rvalue.unkeep() : op; + } + + // Check for assign-update, "x += e0". + // Returns "x + e0"; the caller assigns value. + // if ch is postfix, then lhs is kept and caller will unkeep and return it. + private Node opAssign( char ch, Node lhs, Type t ) { + // RHS of the update. + lhs.keep(); // Alive across parseAsgn + Node rhs = + (byte)ch == 1 ? con( 1) : // var++ + (byte)ch == -1 ? con(-1) : // var-- + parseAsgn(); // var op= rhs + if( !postfix(ch) ) lhs.unkeep(); // Allow to die in next peep + // 4 cases: + // int + int ==>> narrow int + // int + flt ==>> error, caller must fail assigning flt into int + // flt + int ==>> use float op, wrap toFloat() + // flt + flt ==>> use float op + Node op = switch(ch) { + case 1, (char)-1, + '+' -> new AddNode(lhs,rhs); + case '-' -> new SubNode(lhs,rhs); + case '*' -> new MulNode(lhs,rhs); + case '/' -> new DivNode(lhs,rhs); + case '|' -> new OrNode(null,lhs,rhs); + case '&' -> new AndNode(null,lhs,rhs); + case '^' -> new XorNode(loc(),lhs,rhs); + case Lexer.SHL -> new ShlNode(loc(),lhs,rhs); + case Lexer.SAR -> new SarNode(loc(),lhs,rhs); + case Lexer.SHR -> new ShrNode(loc(),lhs,rhs); + default -> throw TODO(); + }; + // Convert to float ops, or narrow int types; error if not declared type. + // Also, if postfix LHS is still keep() + return convertExpr(peep(op),t); + } + + + /** + Parse an allocation + */ + private Node alloc() { + Type t = type(); + if( t==null ) { + String tname = _lexer.matchId(); + if( tname==null || KEYWORDS.contains(tname) ) + throw error("Expected a type"); + t = forwardRefType(tname); + } + // Parse ary[ length_expr ] + if( match("[") ) { + if( !t.makeZero().isa(t) ) + throw error("Cannot allocate a non-nullable, since arrays are always zero/null filled"); + Node len = parseAsgn().keep(); + require("]"); + return allocArray(typeAry(t,false),len); + } else { + if( !(t instanceof TypeMemPtr tmp) ) + throw error("Cannot allocate a "+t.str()); + return allocStruct(tmp._obj); + } + } + + private Node allocStruct(TypeStruct ts) { + Ary ctorArgs = constructorArgs(); + // A field declaration can leave a short forward type name in TYPES. + // Allocation happens in lexical context, so resolve that short name + // to the completed nested type before selecting its class helper. + String typeName = ts._name.indexOf('.') == -1 ? fullTypeName(ts._name) : ts._name; + Type resolved = TYPES.get(typeName); + if( resolved instanceof TypeStruct rts ) ts = rts; + TypeStruct clz = (TypeStruct)TYPES.get(addClzPrefix(typeName)); + if( clz==null ) + throw error("Unknown struct type '" + ts._name + "'"); + Field afld = clz.field(""); + Node alloc; + if( afld != null ) { + TypeFunPtr tfp = (TypeFunPtr)afld._t; + FunNode fun = _code.link(tfp); + alloc = fun==null + ? new FRefNode((addClzPrefix(typeName)+".").intern(),loc()) + : new FunPtrNode(tfp,_code._start,fun.ret()).peephole(); + } else { + if( !ts._fref ) + throw error("Constructor arguments for '" + ts._name + "' but no constructor is defined"); + Type[] sig = new Type[ctorArgs._len]; + for( int i=0; i").intern(),loc(), + TypeFunPtr.make(false,sig,TypeMemPtr.make(ts))); + } + Ary args = new Ary<>(Node.class); + args.add(ctrl()); args.add(mem()); + for( Node arg : ctorArgs ) args.add(arg.unkeep()); + args.add(alloc); + return functionCall(args); + } + + private Ary constructorArgs() { + Ary args = new Ary<>(Node.class); + if( !match("(") ) + return args; + while( !peek(')') ) { + Node arg = parseAsgn(); + if( arg==null ) break; + args.push(arg.keep()); + if( !match(",") ) break; + } + require(")"); + return args; + } + + private Node allocArray(TypeStruct ts, Node len) { + Field lenFld = ts.field("#" ); + Field bodyFld = ts.field("[]"); + + ConFldOffNode base = off(ts, "[]"); + int scale = ts.aryScale(); + Node size = peep(new AddNode(base,peep(new ShlNode(null,len,con(scale))))); + + // Build a NewNode; takes in and puts out all aliases. + NewNode nnn = new NewNode(ts, ctrl(), size ).init(); + ProjNode self = new ProjNode(nnn,0,ts.str()).init().keep(); + ProjNode smem = new ProjNode(nnn,1,"#selfMem").init().keep(); + + // Store length. Rest of array is zero'd via CALLOC during CodeGen. + // Length is casted to sanity. + // TODO: Needs runtime check. + Node st = peep(new StoreNode(null, "#", 1, TypeInteger.U32, null, smem, self, off(ts,"#"), len.unkeep(), true)); + + // TODO: Allow add-on "constructor" to init the array elements + + Node prior = mem(); + Node esc = peep(new EscapeNode(lenFld,self,st,prior)); + mem(mergeAlias(prior,lenFld._alias,esc)); + prior = mem(); + esc = peep(new EscapeNode(bodyFld,self,smem,prior)); + mem(mergeAlias(prior,bodyFld._alias,esc)); + smem.unkeep(); + return self.unkeep(); + } + + private Node newString(String s) { + TypeStruct ts = typeAry(TypeInteger.U8,true); + int lenAlias = ts.field("#" )._alias; + int elemAlias = ts.field("[]")._alias; + TypeConAryB body = TypeConAryB.make(s); + TypeInteger slen = TypeInteger.constant(s.length()); + // Make a TMP, not-null (byte)2, singleton (true) (requires text + // strings are hash-interned), with a TypeStruct having a constant + // array body. + TypeMemPtr str = TypeMemPtr.make((byte)2,TypeStruct.makeAry("[]u8", slen, lenAlias, body, elemAlias, true),true); + assert str.isConstant(); + return con(str.makeRO()); + } + + /** + * Parse postfix expression; this can be a field expression, an array + * lookup or a postfix operator like '#' + * + *
+     *     expr ('.' FIELD)* [ = expr ]       // Field reference
+     *     expr '#'                           // Postfix unary read operator
+     *     expr ['++' | '--' ]                // Postfix unary write operator
+     *     expr ('[' expr ']')* = [ = expr ]  // Array reference
+     *     expr '(' [args,]* ')'              // Function call
+     * 
+ */ + private Node parsePostfixMethod(Node expr, Node self) { + // TODO: This is too broad because the parser lacks strong types here. + // Some normal functions can end up with a "self" argument. + + // Self method call? Self is a TMP-not-CLZ, then assume and pass self. + if( match("(") ) { + expr = parsePostfix(require(functionCall(expr,self),")")); + } else if( expr != self ) + self.isKill(); // Not a method call, no need for self + return parsePostfix(expr); + } + private Node parsePostfix(Node expr) { + _lexer.skipWhiteSpace(); + Lexer site = loc(); + String name; + if( match(".") ) name = requireId(); + else if( match("#") ) name = "#"; + else if( match("[") ) name = "[]"; + // Can get here without an explicit receiver: "f()" or + // "self.method()(next)(next)(next)". Pass the current function self. + else if( match("(") ) return parsePostfix(require(functionCall(expr,defaultSelf()),")")); + else return expr; // No postfix + + return parsePostfixName(expr,name,0,site); + } + + private boolean privateSelf(Node ptr) { + while( ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode ) ptr=ptr.in(1); + Node self=defaultSelf(); + while( self instanceof ConvertNode || self instanceof ReadOnlyNode || self instanceof GuardNode ) self=self.in(1); + return ptr==self; + } + + // An explicit self.x read sees the same current value as bare x. Loading + // the allocation here would miss updates still held in local SSA values. + private Node privateField(String name, int pre) { + int idx=fieldVarIdxBefore(_scope._kinds.at(_scope.enclosingFuncOrDecl())._lexSize,name); + if( idx<0 ) throw error("Cannot resolve field '"+name+"'"); + Var v=_scope.var(idx); + if( pre==0 && matchOpx('=','=') ) { + checkSharedFunction(v); + Node value=parseAsgn().keep(); + _scope.update(v,convertExpr(value,v.type())); + return value.unkeep(); + } + Node value=_scope.in(_scope.update(v,null)); + if( v._uninit && missingInit(value,new BitSet()) ) + throw error("Field '"+name+"' might not be initialized",v._loc); + char ch=pre==0 ? _lexer.matchOperAssign() : (char)pre; + if( ch!=0 ) { + checkSharedFunction(v); + Node op=opAssign(ch,value,v.type()); + _scope.update(v,op); + if( pre!=0 && postfix(ch) ) value.unkeep(); + return pre==0 && postfix(ch) ? value.unkeep() : op; + } + return parsePostfixMethod(value,defaultSelf()); + } + + private void checkSharedFunction(Var v) { + if( _scope.canInit(v) && v._final && v.type() instanceof TypeFunPtr t && t.isConstant() ) + throw error("Cannot reassign shared function '"+v._name+"'"); + } + + // A loop Phi is temporarily bottom before its backedge is parsed. That is + // not an unwritten field: follow its live inputs to the actual defaults. + private boolean missingInit(Node value, BitSet seen) { + if( value==null || seen.get(value._nid) ) return false; + seen.set(value._nid); + if( value instanceof PhiNode phi && !(value instanceof ParmNode) ) { + for( int i=1; i + * expr ['++' | '--' ] // Postfix unary write operator + * expr [ [op]= expr ] // Field reference + * expr ('[' expr ']')* [ [op]= expr ] // Array reference + * + */ + + // pre is +/-1 for a prefix update, otherwise parse a postfix operation. + private Node parsePostfixName(Node expr, String name, int pre) { + return parsePostfixName(expr,name,pre,loc()); + } + + private Node parsePostfixName(Node expr, String name, int pre, Lexer site) { + if( inExplicitConstructor() && privateSelf(expr) ) return privateField(name,pre); + // Keep expr across possible updates + expr.keep(); + + // Get the best known TypeStruct from a pointer type. + TypeStruct ts = null, ts2; + if( expr._type instanceof TypeMemPtr tmp && (ts2=((TypeStruct)TYPES.get(tmp._obj._name))) != null ) + ts = (TypeStruct)tmp._obj.join(ts2); + // A Load's flow type is intentionally not promised during parsing, + // but its declaring field type is still known. Recover that static + // type for chained accesses such as `_grow(1).buf[len++]`; otherwise + // the array Store is parsed as alias #1 and can hide the independent + // side effect of the postfix `len++`. + if( ts==null && expr instanceof LoadNode load ) { + TypeStruct ots = load.off() instanceof ConFldOffNode coff ? coff._ts : null; + if( ots==null && load.ptr()._type instanceof TypeMemPtr owner ) { + Type owner0 = TYPES.get(owner._obj._name); + ots = owner0 instanceof TypeStruct obj ? obj : owner._obj; + } + if( ots != null ) { + Field loaded = ots.field(load._name); + if( loaded != null && loaded._t instanceof TypeMemPtr ltmp ) { + Type loaded0 = TYPES.get(ltmp._obj._name); + ts = loaded0 instanceof TypeStruct obj ? obj : ltmp._obj; + } + } + } + + Field fld = ts==null ? null : ts.field(name); + if( fld==null && ts!=null && !startsClzPrefix(ts._name) ) { + Type clz0 = TYPES.get(addClzPrefix(ts._name)); + if( clz0 instanceof TypeStruct clz && (fld = latestStruct(clz).field(name)) != null ) { + expr.unkeep(); + ts = latestStruct(clz); + expr = con(TypeMemPtr.make((byte)2,ts,true)).keep(); + } + } + if( fld==null && ts!=null ) + fld = fieldOrOpen(ts,name,false); + if( fld==null && ts!=null && !latestStruct(ts)._open ) + throw error("Accessing unknown field '"+name+"' from '*"+ts._name+"'"); + + // We have to record "active touches" of a new class, to track when we + // need to force class initialization. Basically, we insert an CLInit, + // and if it does not fold away it'll turn into a runtime check. + Kind.Func func = (Kind.Func)_scope._kinds.at(_scope.enclosingFuncOrDecl()); + if( (ts==null || ts==TypeStruct.BOT || ts.isHigh() || startsClzPrefix(ts._name)) && + !(ts!=null && (ts._name+".").equals(func._name)) ) + classInit(site,expr); + + // With no field, we will update the bulk memory + int alias = fld==null ? 1 : fld._alias; + Type decl = fld==null ? Type.BOTTOM : fld._t; + + // Field offset; fixed for structs, computed for arrays + Node off = (name=="[]" + // Array element math, with unknown base offset and shift amount. + ? peep(new AddNode(fldoff(expr,"[]"),peep(new ShlNode(null,require(parseAsgn(),"]"),fldoff(expr,"<<"))))) + // Named field offset on an unknown struct type + : fldoff(expr,name)).keep(); + + // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment + if( pre==0 && matchOpx('=','=') ) { + // Field assignment + Node val = parseAsgn().keep(); + // Lift value for store + Node lift = fld==null ? val : new ConvertNode(fld._t,val).peephole(); + // Memory for store, post assignment expression + Node mem = mem(); + // Store to field + mem.keep(); + if( expr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL ) + throw error("Cannot modify final field '"+name+"'"); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), lift, false).peephole().keep(); + storeMem(st,alias,mem); + st.unkeep(); + mem.unkeep(); + + return val.unkeep(); // "obj.a = expr" returns the expression while updating memory + } + + // Memory for load + Node mem = mem(); + // Loading from a constant array, the declared type is the + // meet-over-elements and not the array itself. + if( name=="[]" && decl instanceof TypeConAry conary ) + decl = conary.elem(); + + // Load field + Node load = peep(new LoadNode(loc(),name, alias, ctrl(), mem, expr, off)); + + // Check for assign-update, "ptr.fld += expr" or "ary[idx]++" + char ch = pre==0 ? _lexer.matchOperAssign() : (char)pre; + if( ch!=0 ) { + if( fld!=null && decl==Type.BOTTOM ) + throw error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" ); + Node op = opAssign(ch,load, decl ).keep(); + mem.keep(); + if( expr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL ) + throw error("Cannot modify final field '"+name+"'"); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), op, false).peephole().keep(); + storeMem(st,alias,mem); + st.unkeep(); + mem.unkeep(); + + op.unkeep(); // A narrow Store may have folded away the expression's conversion. + if( postfix(ch) ) load.unkeep(); + return pre==0 && postfix(ch) ? load : op; + } + off.unkill(); + // Might be a method call, so pass expr as 'self' + return parsePostfixMethod(load,expr.unkeep()); + } + + + // zero/sign extend. "i" is limited to either classic unsigned (min==0) or + // classic signed (min=minus-power-of-2); max=power-of-2-minus-1. + private Node zsMask(Node val, Type t ) { + if( val._type instanceof TypeFloat && t instanceof TypeInteger ) + val = new ToIntegerNode(val).peephole(); + if( !(val._type instanceof TypeInteger tval && t instanceof TypeInteger t0 && !tval.isa(t0)) ) { + if( !(val._type instanceof TypeFloat tval && t instanceof TypeFloat t0 && !tval.isa(t0)) ) + return val; + // Float rounding + return peep(new RoundF32Node(val)); + } + if( t0._min==0 ) // Unsigned + return peep(new AndNode(null,val,con(t0._max))); + // Signed extension + int shift = Long.numberOfLeadingZeros(t0._max)-1; + Node shf = con(shift); + if( shf._type==TypeInteger.ZERO ) + return val; + return peep(new SarNode(null,peep(new ShlNode(null,val,shf.keep())),shf.unkeep())); + } + + /** + * Parse a function body; the caller will parse the surrounding "{}" + * + *
+     *     { [type arg,]* -> expr }
+     *     { expr } // The no-argument function
+     * 
+ */ + private Node func() { + Ary ts = new Ary<>(Type.class); + Ary ids = new Ary<>(String.class); + java.util.BitSet finals = new java.util.BitSet(); + Type self = implicitSelfType(); + ts.push(self); + ids.push(self == TypePtr.PTR ? "#self" : "self"); + + // Parse other arguments + _lexer.skipWhiteSpace(); + Lexer loc = loc(); // First argument location + while( true ) { + Type t = type(); // Arg type + if( t==null ) break; + finals.set(ids.size(),bindingFinal(t)); + String id = requireId(); + ts .push(t ); // Push type/arg pairs + ids.push(id); + match(","); + } + require("->"); + // Make a concrete function type, with a fidx + TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),Type.BOTTOM,_code.fidx(_ref._cname)); + ReturnNode ret = parseFunctionBody(tfp,loc,finals,ids.asAry()); + return new FunPtrNode(tfp,_code._start,ret).peephole(); + } + + private Node constructorFunc(String name) { + require("{"); + Ary ts = new Ary<>(Type.class); + Ary ids = new Ary<>(String.class); + java.util.BitSet finals = new java.util.BitSet(); + TypeStruct self = _scope.constructorSelf(); + assert self != null; + ts.push(TypeMemPtr.make(constructorRecv(self))); + ids.push("self"); + // Explicit constructors use the same hidden allocation arguments as + // : public memory is the call memory edge (index 1), followed by + // self (index 2) and the private, unescaped object memory (index 3). + ts.push(TypeMem.makePrivate(self)); + ids.push("#selfMem"); + + _lexer.skipWhiteSpace(); + Lexer loc = loc(); + while( true ) { + Type t = type(); + if( t==null ) break; + finals.set(ids.size(),bindingFinal(t)); + String id = requireId(); + ts .push(t ); + ids.push(id); + match(","); + } + require("->"); + TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),TypeMem.makePrivate(self),_code.fidx(_ref._cname)); + Node[] preParse = _scope.save(); + ReturnNode ret = _parseFunctionBody((self._name+"."+name).intern(),tfp,loc,finals,ids.asAry()); + _scope.restore(preParse); + require("}"); + return new FunPtrNode(tfp,_code._start,ret).peephole(); + } + + /** + * Parse function call arguments; caller will parse the surrounding "()" + *
+     *   ( arg* )
+     * 
+ */ + private Node functionCall(Node fcn, Node self) { + Lexer site = loc(); + site._position--; // Opening '('; arguments can contain nested calls. + if( fcn._type == Type.NIL ) + throw error("Calling a null function pointer"); + fcn.keep(); // Keep while parsing args + + Ary args = new Ary<>( Node.class ); + args.push(null); // Space for ctrl,mem + args.push(null); + if( self != null ) // Method call has a self + args.push(self.keep()); + while( !peek(')') ) { + Node arg = parseAsgn(); + if( arg==null ) break; + args.push(arg.keep()); + if( !match(",") ) break; + } + // Control & memory after parsing args + args.set(0,ctrl().keep()); + args.set(1,mem().keep()); + args.push(fcn); // Function pointer + // Unkeep them all + for( Node arg : args ) + arg.unkeep(); + return functionCall(args,site); + } + + private Node functionCall( Ary args ) { + return functionCall(args,loc()); + } + + private Node functionCall( Ary args, Lexer site ) { + // Dead into the call? Skip all the node gen + if( ctrl()._type == Type.XCONTROL ) { + for( Node arg : args ) + if( arg.isUnused() ) + arg.kill(); + return con(Type.TOP); + } + + // Into the call + CallNode call = (CallNode)new CallNode(site, args.asAry()).peephole(); + // Post-call setup + CallEndNode cend = (CallEndNode)new CallEndNode(call,TypeRPC.constant(_code.rpc(_ref._cname))).peephole(); + call.peephole(); // Rerun peeps after CallEnd, allows early inlining + cend = (CallEndNode)cend.keep().peephole(); // TODO: Might inline + // Control from CallEnd + ctrl(new CProjNode(cend,0,ScopeNode.CTRL).peephole()); + // Memory from CallEnd + Node mem = new ProjNode(cend,1,ScopeNode.MEM0).peephole(); + mem(mem); + // Call result + return new ProjNode(cend.unkeep(),2,"#2").peephole(); + } + + // Just after parsing "type foo = " can parse `"C"` + private boolean isExternDecl( ) { + int old = pos(); + String s = parseString(); + if( "C".equals(s) ) return true; + pos(old); + return false; + } + // External linked constant + private ConstantNode externDecl( String ex, Type t ) { + if( t instanceof TypeFunPtr tfp ) { // Generic TFP from type parse + int fidx = _code.fidx(_ref._cname); // Get a FIDX + tfp = tfp.makeFrom(fidx); // Become a constant + _code.externFunc(fidx,ex); // Map fidx to extern name + t = tfp; + } + return (ExternNode)(new ExternNode(t,ex).peephole()); + } + + // A lexical C binding has a singleton namespace with one external field. + // The namespace occupies no storage; its field offset supplies the address. + private Node externData(String name, Type t) { + if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) ) + throw error("C data binding requires an integer or float type"); + String cname = ("C:"+name).intern(); + TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,_code.externAlias(name),false,true)); + Type old = TYPES.putIfAbsent(cname,ts); + if( old!=null && old!=ts ) + throw error("Conflicting types for C data '"+name+"'"); + return con(TypeMemPtr.make((byte)2,ts,true)); + } + + /** + * Parse integer literal + * + *
+     *     integerLiteral: [1-9][0-9]* | [0]
+     *     floatLiteral: [digits].[digits]?[e [digits]]?[f]?
+     * 
+ */ + private ConstantNode parseLiteral() { return con(_lexer.parseNumber()); } + ConstantNode con( long con ) { return _code.con(con); } + ConstantNode con( Type t ) { return _code.con(t ); } + private void classInit(Lexer site, Node ptr) { + // Ordinary load machinery resolves the flag alias and offset when + // the receiver is still a forward reference. + Field flag = ptr._type instanceof TypeMemPtr tmp ? tmp._obj.field("$init") : null; + int alias = flag==null ? 1 : flag._alias; + Node ready = new LoadNode(site,"$init",alias,null,mem(),ptr,fldoff(ptr,"$init")).peephole(); + CLInitNode init = new CLInitNode(site,ctrl(),mem(),ptr,ready).peephole().keep(); + ctrl(new CProjNode(init,0,ScopeNode.CTRL).peephole()); + Node bulk = new ProjNode(init,1,ScopeNode.MEM0).peephole().keep(); + flag = init.flag(); + mem(flag==null ? bulk : mergeAlias(bulk,flag._alias, + new ProjNode(init,2,memName(flag._alias)).peephole())); + bulk.unkeep(); + init.unkeep(); + } + + // Field offset for a known type (but unknown offset) + public static ConFldOffNode off( TypeStruct ts, String fname ) { + return (ConFldOffNode)(new ConFldOffNode(ts,fname).peephole()); + } + // Field offset for an unknown type + public Node fldoff( Node n, String name ) { + return new FldOffNode(n,name,loc()).peephole(); + } + public Node peep( Node n ) { + // Peephole, then improve with lexically scoped guards + return _scope.upcastGuard(n.peephole()); + } + + // Parse a string or null + private String parseString() { + if( !peek('"') ) return null; + _lexer.inc(); + StringBuilder sb = new StringBuilder(); + while( !_lexer.isEOF() ) { + char c = _lexer.nextChar(); + if( c=='"' ) return sb.toString(); + if( c!='\\' ) { + sb.append(c); + continue; + } + if( _lexer.isEOF() ) + throw error("Unclosed string"); + sb.append(escapedChar("string")); + } + throw error("Unclosed string"); + } + + private char escapedChar(String kind) { + char esc = _lexer.nextChar(); + return switch( esc ) { + case '0' -> '\u0000'; + case 'n' -> '\n'; + case 't' -> '\t'; + case 'r' -> '\r'; + case '\\' -> '\\'; + case '"' -> '"'; + case '\'' -> '\''; + default -> throw error("Unknown "+kind+" escape \\\\"+esc); + }; + } + + // Already parsed "'" + private Node parseChar() { + if( _lexer.isEOF() ) throw error("Unclosed character"); + char c = _lexer.nextChar(); + if( c=='\\' ) { + if( _lexer.isEOF() ) throw error("Unclosed character"); + c = escapedChar("character"); + } + return require(con(TypeInteger.constant(c)),"'"); + } + + private boolean noLowerCase(String s) { + for( int i=0; i N require(N n, String syntax) { + if (match(syntax)) return n; + throw errorSyntax(syntax); + } + + //////////////////////////////////// + // Lexer components + + // Lexer provides low level access to the raw file bytes, peeks and matches + // short strings, parses numbers, skips comments and whitespace, tracks + // line numbers, allows the parse position to be saved and restored, and + // serves as a location indicator for errors. + + public static class Lexer { + + // Input buffer; an array of text bytes read from a file or a string + private final byte[] _input; + public final CompUnit _cu; // Original source, retained across inlining. + // Tracks current position in input buffer + private int _position = 0; + + // + private int _line_number = 1; + // Start of current line + private int _line_start = 0; + + /** True when this location precedes {@code that} in the same source. */ + public boolean before(Lexer that) { + return that != null && (_cu!=null ? _cu==that._cu : _input==that._input) && _position < that._position; + } + + public String position() { return _line_number+":"+(_position-_line_start+1); } + public int pos() { return _position; } + + /** + * Record the source text for lexing + */ + public Lexer(String source) { + this(source.getBytes()); + } + + /** + * Direct from disk file source + */ + public Lexer(byte[] buf) { + _input = buf; + _cu = null; + } + + private Lexer(CompUnit cu) { + _input = cu._src.getBytes(); + _cu = cu; + } + + // Imported location: source text is only needed when reporting an error. + private Lexer(CompUnit cu, int pos) { + _input = null; + _cu = cu; + _position = pos; + } + + + /** + * Copy a lexer from a lexer + */ + private Lexer(Lexer l) { + _input = l._input; + _cu = l._cu; + _position = l._position; + _line_number = l._line_number; + _line_start = l._line_start; + } + + // Very handy in the debugger, shows the unparsed program + @Override + public String toString() { + return _input==null ? _cu._cname+"@"+_position : 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]; + } + // Just crash if misused + public byte peek(int off) { return _input[_position+off]; } + + private void inc() { + if( _position++ < _input.length && _input[_position-1] == '\n' ) { + _line_number++; + _line_start = _position; + } + } + // Does not honor LF, so caller must roll back position on a LF + private char nextChar() { + char ch = peek(); + inc(); + 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( true ) { + if( isWhiteSpace() ) inc(); + // Skip // to end of line + else if( _position+2 < _input.length && + _input[_position ] == '/' && + _input[_position+1] == '/') { + inc(); inc(); + while( !isEOF() && _input[_position] != '\n' ) + inc(); + } else if( _position+2 < _input.length && + _input[_position ] == '/' && + _input[_position+1] == '*') { + // Skip /*comment*/ + while( !isEOF() && !(_input[_position-1] == '*' && _input[_position] == '/')) + inc(); + inc(); + } else break; + } + } + + // Next non-white-space character, or EOF + public char nextXChar() { skipWhiteSpace(); return nextChar(); } + + // 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) { + assert syntax.indexOf('\n')==-1; // No newlines in match + 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; + } + // Match this char, and the next char must be different. + // Handles '&&' vs '&' + boolean matchOp( char c0 ) { + skipWhiteSpace(); + if( _position+1 >= _input.length || _input[_position]!=c0 || _input[_position+1]==c0 ) + return false; + inc(); + return true; + } + // Match these two characters in a row + boolean matchOpx(char c0, char c1) { + skipWhiteSpace(); + if( _position+1 >= _input.length || _input[_position]!=c0 || _input[_position+1]==c1 ) + return false; + inc(); + return true; + } + + 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 "EOF"; + if (isIdStart(peek())) return parseId(); + if (isNumber(peek())) return parseNumberString(); + if (isPunctuation(peek())) return parsePunctuation(); + return String.valueOf(peek()); + } + + + boolean isNumber(char ch) {return Character.isDigit(ch);} + + // Return a constant Type, either TypeInteger or TypeFloat + private Type parseNumber() { + int old = _position; + int len = isLongOrDouble(); + boolean f32 = peek()=='f'; + if( f32 ) nextChar(); + int n = Math.abs(len); + if( f32 ) + return TypeFloat.constant(Float.parseFloat(new String(_input,old,n))); + if( len > 0 ) { + if( len > 1 && _input[old]=='0' ) + throw error("Syntax error: integer values cannot start with '0'",this); + long i = Long.parseLong(new String(_input,old,len)); + return TypeInteger.constant(i); + } + return TypeFloat.constant(Double.parseDouble(new String(_input,old,n))); + } + private String parseNumberString() { + int old = _position; + int len = Math.abs(isLongOrDouble()); + if( peek()=='f' ) { + nextChar(); + len++; + } + return new String(_input,old,len); + } + + // Return +len that ends a long + // Return -len that ends a double + private int isLongOrDouble() { + int old = _position; + char c; + while( Character.isDigit(c=nextChar()) ) ; + if( !(c=='e' || c=='.') ) + return --_position - old; + if( peek() == '-' ) nextChar(); + while( Character.isDigit(c=nextChar()) || c=='e' || c=='.' ) ; + return -(--_position - old); + } + + // 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).intern(); + } + + // + private boolean isPunctuation(char ch) { + return "=;[]<>()+-/*&|^".indexOf(ch) != -1; + } + + private String parsePunctuation() { + int start = _position; + return new String(_input, start, 1); + } + + // Next oper= character, or 0. + // As a convenience, mark "++" as a char 1 and "--" as char -1 (65535) + // Distinct tags for the multi-character compound operators. + static final char SHL = 2, SAR = 3, SHR = 4; + + public char matchOperAssign() { + skipWhiteSpace(); + if( match("<<=" ) ) return SHL; + if( match(">>>=") ) return SHR; + if( match(">>=" ) ) return SAR; + if( _position+2 >= _input.length ) return 0; + char ch0 = (char)_input[_position]; + if( "+-/*&|^".indexOf(ch0) == -1 ) return 0; + char ch1 = (char)_input[_position+1]; + if( ch1 == '=' ) { _position += 2; return ch0; } + if( isIdLetter((char)_input[_position+2]) ) return 0; + if( ch0 == '+' && ch1 == '+' ) { _position += 2; return (char) 1; } + if( ch0 == '-' && ch1 == '-' ) { _position += 2; return (char)-1; } + return 0; + } + } + + ParseException errorSyntax(String syntax) { return _errorSyntax("expected `"+syntax+"`"); } + private ParseException _errorSyntax(String msg) { + return error("Syntax error, "+msg+" but found `" + _lexer.getAnyNextToken()+"`"); + } + ParseException error(String msg) { return error(msg,_lexer); } + public static ParseException error(String msg, Lexer loc) { return new ParseException(msg,loc); } + + public static class ParseException extends RuntimeException { + public final Lexer _loc; + // file:line:charoff err + //String msg = "src:"+_line_number+":"+(_position-_line_start)+" "+errorMessage; + ParseException( String msg, Lexer loc ) { super(msg); _loc = loc; } + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/Var.java b/chapter-modules/src/main/java/com/seaofnodes/simple/Var.java new file mode 100644 index 00000000..af371d21 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/Var.java @@ -0,0 +1,54 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + + +/** + * The tracked fields are now complex enough to deserve a array-of-structs layout + */ +public class Var { + + public final String _name; // Declared name + // These fields are not final for forward reference late updates or + // promotions. + public int _idx; // index in containing scope + private Type _type; // Declared type + public boolean _final; // Final field + public boolean _fref; // Forward ref + // Parser-only definite-initialization marker. The current binding's + // nullable type carries the path-sensitive state through SSA merges. + public boolean _uninit; + public int _extern; // C storage alias; zero for ordinary variables + public Parser.Lexer _loc; // Source location + + public Var(int idx, String name, Type type, boolean xfinal, Parser.Lexer loc ) { + this(idx,name,type,xfinal,loc,false); + } + public Var(int idx, String name, Type type, boolean xfinal, Parser.Lexer loc, boolean fref) { + _idx = idx; + _name = name; + _type = type; + _final = xfinal; + _fref = fref; + _loc = loc; + } + public Type type() { return _type; } + public Type _type() { return _type; } // ScopeNode printer needs to have no side effects + + // Forward reference variables (not types) must be BOTTOM and + // distinct from inferred variables + public boolean isFRef() { return _fref; } + + public void defFRef( Type type, boolean xfinal, Parser.Lexer loc ) { + assert isFRef() && xfinal; + _type = type; + _final = true; + _fref = false; + _loc = loc; + } + + @Override public String toString() { + return (_fref ? "FREF " : "")+(_uninit ? "UNINIT " : "")+_type.toString()+(_final ? " ": " !")+_name; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java new file mode 100644 index 00000000..0b314978 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java @@ -0,0 +1,101 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; + +abstract public class BuildLRG { + // Compute live ranges in a single forwards pass. Every def is a new live + // range except live ranges are joined at Phi. Also, gather the + // intersection of register masks in each live range. Example: "free + // always zero" register forces register zero, and calling conventions + // typically force registers. + + // Sets FAILED to the set of hard-conflicts (means no need for an IFG, + // since it will not color, just split the conflicted ranges now). Returns + // true if no hard-conflicts, although we still might not color. + public static boolean run(int round, RegAlloc alloc) { + FunNode fun = null; + for( Node bb : alloc._code._cfg ) { + if( bb instanceof FunNode fun2 ) + fun = fun2; + for( Node n : bb.outs() ) { + // A synthetic never-exit value has no runtime definition. + if( n._type==Type.TOP ) continue; + if( n instanceof PhiNode phi && !(phi._type instanceof TypeMem) ) { + if( phi instanceof MachNode mach && mach.outregmap()==null ) + continue; + // All real Phi inputs end up with the same LRG. + // Pass 1: find any pre-existing LRG, to avoid make-then-Union a LRG + LRG lrg = alloc.lrg(phi); + if( lrg == null ) + for( int i=1; i fcns from +// CompUnits from CLInitNodes, and the set of fcn call targets in f and the +// class parent. These are all reported as fidxs (ints). +// +// (2) Walk the _edges graph using Tarjans SCC. Reject cycles containing more +// than one . +// +// (3) Report on cycles +// +public class ClassInitDependencies { + private final CodeGen _code; + // Function FIDX -> callees, touched initializers, and parent initializer. + public final int[][] _edgess; + private final int[] _pres; + private final int[] _lows; + private final int[] _stk ; + private int _sp; + + + // Build once; tests can inspect the graph independently of cycle checking. + public ClassInitDependencies( CodeGen code ) { + _code = code; + assert code._phase.ordinal() >= CodeGen.Phase.IterPostOpto.ordinal(); + _pres = new int[code._linker._len]; + _lows = new int[code._linker._len]; + _stk = new int[code._linker._len]; + + // + BitSet visit = code.visit(); // Visit set per-fcn + Ary work = new Ary<>(CFGNode.class); // Worklist per-fcn + IntHashMap iset = new IntHashMap(0); // dedup hash set per-fcn + _edgess = new int[code._linker._len][]; + + // For all functions, build the _edges array + for( FunNode fun : code._linker ) + if( fun!=null && !fun.isDead() ) + // Visit function and collect direct class touches and callees + _edgess[fun.sig().fidx()] = walk(code,fun,work,iset,visit,null); + visit.clear(); + } + + // Return the outgoing init edge set for this function + private static int[] walk( CodeGen code, FunNode fun, Ary work, IntHashMap iset, BitSet visit, + Ary sites) { + // Walk function body, looking for outgoing init edges + assert iset .isEmpty(); // int hash set for de-dup + assert visit.isEmpty(); // needs a visit set, because loops + assert work .isEmpty(); // needs a worklist, because DAG, parallel explore paths + + // Children classes always dependent on parents + if( fun.isClz() && fun._compunit._par != null ) + iset.put(code.link(fun._compunit._par).sig().fidx(),1); + + work.push(fun); + while( !work.isEmpty() ) { + CFGNode cfg = work.pop(); + if( visit.get(cfg._nid) ) continue; // Been there, done that + visit.set(cfg._nid); + assert !cfg._type.isHigh(); + // Do not walk off end of function + if( cfg instanceof ReturnNode ) continue; + + // CLInit adds a class init requirement to this function + // (plus all parent classes) + if( cfg instanceof CLInitNode init ) { + FunNode clz = code.link(init.cu()); // referred to + iset.put(clz.sig().fidx(),1); // Add to edge set + if( sites!=null && sites.atX(clz.sig().fidx())==null ) sites.setX(clz.sig().fidx(),init._loc); + } + + // Calls union all their fptr fidxs into the callee set + if( cfg instanceof CallNode call && call.fptr()._type instanceof TypeFunPtr fp ) { + assert !XInt.isHigh(fp.fidxs()) && fp.nfcns()>0; // Call is alive with valid targets + // Union call targets into the callee set + for( int dstx = XInt.next(fp.fidxs(),0); dstx>=0; dstx=XInt.next(fp.fidxs(),dstx) ) { + iset.put(dstx,1); + if( sites!=null && sites.atX(dstx)==null ) sites.setX(dstx,call._loc); + } + } + + // Forward recursion to walk the whole function + for( Node use : cfg._outputs ) { + // Do not walk into another function + if( !(use instanceof CFGNode cfg1) || use instanceof FunNode || use instanceof StopNode ) continue; + // Expect control flow edge is sensible + assert (use instanceof RegionNode r && r._inputs.find(cfg)>0) || use.in(0)==cfg; + if( !visit.get(cfg1._nid) ) // Small speed opt + work.add(cfg1); + } + } + + visit.clear(); + int[] edges = iset.isEmpty() ? null : iset.keySet(); + iset.clear(); + return edges; + } + + // Return an error string, or null if no cycle contains multiple clinits. + public String check() { + Arrays.fill(_pres,0); + _sp = 0; + _pres[0] = 1; // Preorders stored in pres[0] and start at 1 + + int idx = -1; + for( int fidx = 1; fidx < _edgess.length; fidx++ ) + if( _pres[fidx] == 0 && // Already visited + (idx = scc(fidx)) != -1 ) // And found a cycle + return report(idx); // Report cycle + return null; + } + + // Every internal edge of an SCC belongs to a cycle. Print all of them: + // stack order is not a path, and enumerating all cycle paths can explode. + // This bookkeeping and formatting are only needed on an error. + private String report(int idx) { + BitSet members = new BitSet(); + for( int i=idx; i<_sp; i++ ) members.set(_stk[i]); + SB sb = new SB().p("Cyclic class initialization:"); + // Recover one surviving source site per edge only after finding an + // error. A representative touch is enough; runtime first-touch order + // need not be statically known. Lexer snapshots retain the source CU + // even when the node has been inlined into a different function. + Ary work = new Ary<>(CFGNode.class); + IntHashMap iset = new IntHashMap(0); + BitSet visit = new BitSet(); + Ary sites = new Ary<>(Parser.Lexer.class); + for( int i=idx; i<_sp; i++ ) { + int src = _stk[i]; + FunNode fun = _code.link(src); + Arrays.fill(sites._es,null); + sites.clear(); + walk(_code,fun,work,iset,visit,sites); + if( _edgess[src]!=null ) + for( int dst : _edgess[src] ) + if( members.get(dst) ) { + sb.p("\n ").p(name(src)).p(" -> ").p(name(dst)); + Parser.Lexer loc = sites.atX(dst); + if( loc!=null && loc._cu!=null ) + sb.p('\n').p(SourceError.format(loc._cu,loc.pos(),"cyclic class initialization")); + else if( fun.isClz() && fun._compunit._par==_code.link(dst)._compunit ) + sb.p("\n parent initialization requirement"); + } + } + return sb.toString(); + } + + private String name(int fidx) { + FunNode fun = _code.link(fidx); + String owner=fun._compunit._cname; + if( fun.isClz() ) return owner; + String label=fun.label(); + return (label.startsWith(owner+".") ? label : owner+"."+label)+" [fidx "+fidx+"]"; + } + + // Tarjan SCC + int scc( int fidx ) { + // Assign pre-order number + _lows[fidx] = _pres[fidx] = _pres[0]++; + _stk[_sp++] = fidx; + // walk outbound edges + if( _edgess[fidx]!=null ) { + for( int edge : _edgess[fidx] ) { + if( edge >= _pres.length ) continue; // Out of bounds happens when calling C fcns + if( _pres[edge] == 0 ) { // Not visited + int idx = scc(edge); + if( idx != -1 ) return idx; // Found a cycle + _lows[fidx] = Math.min(_lows[fidx],_lows[edge]); + } else if( _edgess[edge]!=null && onStk(edge) ) { + _lows[fidx] = Math.min(_lows[fidx],_pres[edge]); + } + } + } + // Found a cycle + if( _lows[fidx] == _pres[fidx] ) { + int idx = find(fidx); + if( hasClzCycle(idx) ) + return idx; // Found a class cycle + // Pop stack, including fidx + _sp = idx; + } + return -1; // No cycle + } + + private int find(int e) { + for( int i=0; i<_sp; i++ ) if( _stk[i] == e ) return i; + return -1; + } + private boolean onStk(int e) { return find(e) != -1; } + + private boolean hasClzCycle(int idx) { + int clinits = 0; + for( int i=idx; i<_sp; i++ ) { + FunNode fun = _code.link(_stk[i]); + if( fun!=null && fun.isClz() && ++clinits > 1 ) + return true; + } + return false; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java new file mode 100644 index 00000000..689ceceb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java @@ -0,0 +1,105 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import java.util.BitSet; +import java.util.IdentityHashMap; + +// Coalesce copies +abstract public class Coalesce { + + public static boolean coalesce( int round, RegAlloc alloc ) { + + // Walk all the splits, looking for coalesce chances + boolean progress = false; + for( CFGNode bb : alloc._code._cfg ) { + outer: + for( int j=0; j v2.nadj() ) + { v1 = v2; v2 = alloc.lrg(n); } + int v2len = v2._adj==null ? 0 : v2._adj._len; + // See that they do not conflict (coalescing would make a self-conflict) + if( v1._adj!=null && v2._adj!=null && v1._mask.overlap(v2._mask) ) { + // Walk the smaller neighbors, and add to the larger. + // Check for direct conflicts along the way. + for( LRG v3 : v1._adj ) { + if( v3==v2 ) { + // Direct conflict, skip this LRG pair + v2._adj.setLen(v2len); + continue outer; + } + // Add missing LRGs from V1 to V2 + if( v2._adj.find(v3)== -1 ) + v2._adj.push(v3); + } + } + // Most constrained mask + RegMask mask = v1._mask==v2._mask ? v1._mask : v1._mask.copy().and(v2._mask); + // Check for capacity + if( v2.nadj() >= mask.size() ) { + // Fails capacity, will not be trivial colorable + if( v2._adj!=null ) v2._adj.setLen(v2len); + continue; + } + + // Union lrgs. + progress = true; + + // Since `n` is dying, remove from the LRGS BEFORE doing + // the union; this will let the union code keep the + // matching bits from the other side, preserving some + // info for biased coloring + if( ov1. _machDef==n ) ov1. _machDef = null; + if( ov1._splitDef==n ) ov1._splitDef = null; + if( ov2. _machUse==n ) ov2. _machUse = null; + if( ov2._splitUse==n ) ov2._splitUse = null; + // Keep larger adjacency list. + Ary larger = v2._adj; + v2._adj = null; + + // Union lrgs. Swaps again, based on small lrg + LRG vWin = v1.union(v2); + vWin._adj = larger; + LRG vLose = v1.leader() ? v2 : v1; + + // LRGs below v2len were always in v2, but maybe not in v1. + // LRGs above v2len are in v1 but were not in v2. + + // Walk v2's neighbors, replacing vLose with vWin. + if( larger != null ) + for( LRG vn : larger ) { + int idx = vn._adj.find(vLose); + if( idx != -1 ) + vn._adj.del(idx); + if( vn._adj.find(vWin) == -1 ) + vn._adj.add(vWin); + } + } else { + + // Since `n` is dying, remove from the LRGS + if( v1. _machDef==n ) v1. _machDef=null; + if( v1. _machUse==n ) v1. _machUse=null; + if( v1._splitDef==n ) v1._splitDef=null; + if( v1._splitUse==n ) v1._splitUse=null; + } + + // Remove junk split + n.removeSplit(); + j--; + } + } + + if( progress ) + alloc.unify(); + + return true; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java new file mode 100644 index 00000000..e6bace8c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -0,0 +1,625 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.graph.GraphObserver; +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.print.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.io.File; +import java.util.*; + + +// Compile Driver for a Compilation Unit +@SuppressWarnings("unchecked") +public class CodeGen { + + // Last created CodeGen as a global; used all over to avoid passing about a "context". + public static CodeGen CODE; + + // Location of built-in CPU ports + public static final String PORTS = "com.seaofnodes.simple.node.cpus"; + + // Compile phases + public enum Phase { + Parse, // Parse ASCII text into Sea-of-Nodes IR + Iter, // Pessimistic peepholes after parse + Opto, // SCCP, numeric modes, seed the peephole worklist + IterPostOpto, // Peepholes and inlining using SCCP results + FinalizeIR, // Field layout, offset cleanup, function/CU roots + TypeCheck, // Last check for bad programs + Lower, // Expand checked class initialization to ordinary IR + LoopTree, // Build a loop tree; break infinite loops + Select, // Convert to target hardware nodes + Schedule, // Global schedule (code motion) nodes + LocalSched, // Local schedule + RegAlloc, // Register allocation + Encoding, // Encoding + Export, // Export + LastPhase // After last phase + } + public Phase _phase; + // Null for normal compilation; owned by this compilation, not a global viewer. + public GraphObserver _obs; + + /** True when tests deliberately randomize Iter worklist order. */ + public static boolean iterSeedOverridden() { return System.getProperty("simple.iter.seed") != null; } + + // Expensive assert sampling: + // -Dsimple.assert.expensive=0 disables these checks + // -Dsimple.assert.expensive=1 checks every step + // -Dsimple.assert.expensive=N checks once every 1< 0; } + public static boolean expensiveAssert( int trip ) { + if( EXPENSIVE_ASSERT_LOG <= 0 ) return false; + if( EXPENSIVE_ASSERT_LOG == 1 ) return true; + int shift = Math.min(EXPENSIVE_ASSERT_LOG,30); + return (trip & ((1< _compunits; + + // Test shortcut for only one compilation unit + public CompUnit compunit() { + assert _compunits.size()==1; + for( CompUnit cu : _compunits.values() ) + return cu; + assert false : "no compilation unit"; + return null; + } + + // True when fidx is published through a public class or instance field. + public boolean publicFIDX(int fidx) { + if( fidx < LocalIds.RESERVED ) return false; + // A public class initializer is itself an external root; unlike + // constructors and methods, it is not reached through a class field. + FunNode fun = _linker.atX(fidx); + if( fun != null && fun.isClz() && fun.isPublic() ) + return true; + // Include instance methods even while their constant function fields + // are stored in each instance rather than on the class object. + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct clz && + Parser.startsClzPrefix(clz._name) && + Parser.isPublicClassName(clz._name) && + resolvedClass(clz) ) { + if( publicFieldFIDX(clz,fidx) ) return true; + Type self = Parser.TYPES.get(clz._name.substring(Parser.CLZ.length())); + if( self instanceof TypeStruct ts && publicFieldFIDX(ts,fidx) ) return true; + } + return false; + } + + private static boolean publicFieldFIDX(TypeStruct clz, int fidx) { + boolean hasCtor = clz.field("") != null; + for( Field fld : clz._fields ) + if( fld._fname.charAt(0)!='_' && fld._t instanceof TypeFunPtr tfp && + // With a user constructor, class:N.N is the private . + !(hasCtor && clz.hiddenInit(fld)) && + XInt.bit(tfp._fidxs,fidx) ) + return true; + return false; + } + + public static boolean hasUserConstructor(TypeStruct self) { + Type t = Parser.TYPES.get(Parser.addClzPrefix(self._name)); + return t instanceof TypeStruct clz && clz.field("") != null; + } + + // A forward reference gets a class object and constructor signature so + // parsing can proceed, but it has no callable definition yet. + private static boolean resolvedClass(TypeStruct clz) { + Type t = Parser.TYPES.get(clz._name.substring(Parser.CLZ.length())); + return !(t instanceof TypeStruct ts) || !ts._fref; + } + + private int[] _publicFIDXs, _publicAliases; + + /** + * Freeze the externally visible portion of the program after ParseAll has + * loaded every compilation unit and closed/upgraded all types. Later + * optimization can remove linker entries and create inline-only fidxs; + * neither operation changes this interface. + */ + void freezePublicInterface() { + assert _publicFIDXs==null && _publicAliases==null; + int[] fidxs = XInt.EMPTY; + for( int fidx=LocalIds.RESERVED; fidx<_linker._len; fidx++ ) + if( publicFIDX(fidx) ) + fidxs = XInt.make(fidxs,fidx); + int[] aliases = XInt.EMPTY; + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct clz && + Parser.startsClzPrefix(clz._name) && + Parser.isPublicClassName(clz._name) && + resolvedClass(clz) ) { + boolean hasCtor = clz.field("") != null; + for( Field fld : clz._fields ) + if( fld._fname.charAt(0)!='_' && fld._t!=Type.TOP && + !(hasCtor && clz.hiddenInit(fld)) ) + aliases = XInt.make(aliases,fld._alias); + } + _publicFIDXs = fidxs; + _publicAliases = aliases; + } + + public int[] publicFIDXs() { assert _publicFIDXs !=null; return _publicFIDXs; } + public int[] publicAliases(){ assert _publicAliases!=null; return _publicAliases; } + public boolean publicInterfaceFrozen() { return _publicFIDXs != null; } + + // --------------------------- + + // Very common nodes, cached here + public final ConstantNode ZERO; + public final XCtrlNode XCTRL; + + public Node constant( Type t ) { + assert _phase==null || _phase.ordinal() <= Phase.Lower.ordinal(); + return ConstantNode.make(t).peephole(); + } + public ConstantNode con( Type t ) { return (ConstantNode)constant(t); } + public ConstantNode con( long con ) { return con==0 ? ZERO : con(TypeInteger.constant(con)); } + + + // --------------------------- + // Test setup; no module nor file with specific argument + public CodeGen( String src ) { this(src, 126L, true ); } + + // Test setup; no module nor file; can alter seed & argument; can re-run same CodeGen + public CodeGen( String src, Type arg ) { + this(null,null,null, src, 126L, true, arg); + } + // Test setup; no module nor file; can alter seed & argument; can re-run same CodeGen + public CodeGen( String src, long workListSeed, boolean resetTypes ) { + this(null,null,null, src, workListSeed, resetTypes, TypeInteger.BOT); + } + + // Generic CodeGen, including full module setup + public CodeGen( String modDir, String buildDir, + String srcName, String src, long workListSeed, boolean resetTypes, Type arg ) { + // Public singleton to avoid passing about this state to a huge count + // of places. Probably becomes a TLS at some point. + CODE = this; + if( resetTypes ) Type.reset(); // Reset to recover from failed test + + _cwd = System.getProperty("user.dir")+"/"; + _modDir = modDir == null ? _cwd : modDir; + _buildDir = buildDir == null ? _cwd : buildDir; + _srcName = srcName; + _callingConv = null; // Calling convention + // Source code from test strings, not files + _src = src; + // All the compilation units + _compunits = new HashMap<>(); + _phase = null; + // Start GVN table + _gvn = new HashMap<>(); + // Allow whole-suite worklist-order sweeps without rewriting the many + // explicit seeds used by tests and command-line compilation. + String iterSeed = System.getProperty("simple.iter.seed"); + _iter = new IterPeeps(iterSeed == null ? workListSeed : Long.parseLong(iterSeed)); + // End points of graph + _stop = new StopNode().init(); + _start = new StartNode(null,_stop,arg).init(); + ZERO = con(TypeInteger.ZERO).keep(); + XCTRL = new XCtrlNode().peephole().keep(); + P = new Parser(this); + } + + + // Run the requested phases. ELF output contains machine code and data. + + // No code emission, just IR generation + public CodeGen driver( Phase phase ) { return driver(phase,null,null,false,false,0); } + // No object file writing, but code generation for a specific cpu/os pair (allows emulation) + public CodeGen driver( Phase phase, String cpu, String callingConv ) { + if( phase.ordinal() < Phase.Export.ordinal() ) + // No code outputted + return driver(phase,cpu,callingConv,false,false,0); + if( _srcName == null ) + throw new RuntimeException("No source filename provided, so do not know how to name the obj file"); + boolean emitEntrySymbol = !_srcName.contains("."); + return driver( phase, cpu, callingConv, false, emitEntrySymbol, 0 ); + } + // Write an object file for a specific cpu/os pair + + public CodeGen driver( String cpu, String callingConv, boolean inMemory, boolean emitEntrySymbol ) { return driver(Phase.Export,cpu,callingConv,inMemory,emitEntrySymbol,0); } + + // The Generic driver + public CodeGen driver( Phase target, String cpu, String callingConv, boolean inMemory, boolean emitEntrySymbol, int dump ) { + // Parse the entry source and discover all referenced source files. + step(target, Phase.Parse, this::parse, dump); + // Run peepholes to a fixed point + step(target, Phase.Iter, this::iter, dump); + // Run SCCP (and resolve int/flt expressions) + step(target, Phase.Opto, this::opto, dump); + // Run peepholes to a fixed point again, on the better types. + step(target, Phase.IterPostOpto, this::iterPostOpto, dump); + // Field layouts; field symbols become constant offsets + step(target, Phase.FinalizeIR, this::finalizeIR, dump); + // Final error check - bad programs fail here + step(target, Phase.TypeCheck, this::typeCheck, dump); + // Replace "fat" nodes with their exploded explicit IR; this can + // introduce CFG diamonds + step(target, Phase.Lower, this::lower, dump); + // Compute the loop-tree, used for approximating exec frequencies for + // scheduling and register allocation + step(target, Phase.LoopTree, this::loopTree, dump); + // Instruction selection + step(target, Phase.Select, () -> instSelect(cpu,callingConv), dump); + // Global Code Motion + step(target, Phase.Schedule, this::GCM, dump); + // Local (basic block only) scheduling + step(target, Phase.LocalSched, this::localSched, dump); + // Register allocation + step(target, Phase.RegAlloc, this::regAlloc, dump); + // Encode machine instructions into bits + step(target, Phase.Encoding, this::encode, dump); + // Export machine code and data into one ELF file + step(target, Phase.Export, () -> exportELF(inMemory,emitEntrySymbol), dump); + return this; + } + + // The driver names the order; optional phases decide locally whether to run. + private void step(Phase target, Phase next, Runnable work, int dump) { + if( next.ordinal() > target.ordinal() || (_phase!=null && _phase.ordinal()>=next.ordinal()) ) return; + Phase prior = _phase; + work.run(); + if( _phase!=prior ) dump(dump); + } + + // All phase entry points share ordering checks, timing, and viewer notification. + // Record elapsed time even on failure, but report completion only on success. + CodeGen phase(Phase next, boolean ordered, Runnable work) { + assert ordered : "Cannot run "+next+" after "+_phase; + _phase = next; + long t0 = System.currentTimeMillis(); + work.run(); // Do The Phase + _times[next.ordinal()] = System.currentTimeMillis() - t0; + if( _obs!=null ) _obs.phase(next.name()); // Record for viewer + return this; + } + + public String entryClinitName() { + return Parser.addClzPrefix(_srcName==null ? "Test" : _srcName.replace('/','.'))+"."; + } + + // Verbose printing during compilation + private void dump(int dump) { + int p2 = _phase.ordinal(); + if( (dump & (1<= Phase.LocalSched.ordinal() + ? asm(new SB()).toString() + : IRPrinter.prettyPrint(this)); + } + + + // Record times by phase + public final long[] _times = new long[Phase.LastPhase.ordinal()]; + + // --------------------------- + /** + * A counter, for unique node id generation. Starting with value 1, to + * avoid bugs confusing node ID 0 with uninitialized values. + */ + public int _uid = 1; + public int UID() { return _uid; } + public int getUID() { + return _uid++; + } + + + // Allocate dense alias/function/call IDs within this invocation. + // Class slots let a forward reference reserve an initializer's identity. + public final LocalIds _aliases = new LocalIds(); + public int alias(String clz) { return _aliases.next(clz); } + + // C storage has one global alias per symbol, including across object files. + public int externAlias(String name) { return _aliases.next("C:"+name,LocalIds.RESERVED); } + + // External data addresses supplied by an in-memory linker (image offsets). + public final HashMap _externDataAddresses = new HashMap<>(); + + // Compute local function index (FIDX) from global function info. This is + // called *in order* during parsing, and that order is part of the global + // unique mapping + public final LocalIds _fidxs = new LocalIds(); + // Return local index for specific global file & order. Used to find fidxs for e.g. FREF class fcns + public int fidx( String clz, int order ) { return _fidxs.next(clz, order); } + // New fidx in the current file + public int fidx( String clz ) { return fidx(clz, -1); } + + // Compute local RPC index from global RPC info, one per call + public final LocalIds _rpcs = new LocalIds(); + public int rpc( String clz ) { return _rpcs.next(clz); } + + + // Inlining can insert CFG edges and invalidate cached depth ordering. + private char _iDepthVersion; + public char iDepthVersion() { return _iDepthVersion; } + public void invalidateIDepthCaches() { + assert _iDepthVersion < Character.MAX_VALUE : "Dominator cache version exceeds 65535"; + ++_iDepthVersion; + } + + // Popular visit bitset, declared here, so it gets reused all over + public final BitSet _visit = new BitSet(); + public BitSet visit() { assert _visit.isEmpty(); return _visit; } + + // Start and Stop; end points of the generated IR + public StartNode _start; + public StopNode _stop; + + // Global Value Numbering. Hash over opcode and inputs; hits in this table + // are structurally equal. + public final HashMap _gvn; + + // Reverse from a constant function pointer to the IR function being called. + // Error to call with a non-constant TFP + public FunNode link( TypeFunPtr tfp ) { return link(tfp.fidx()); } + // Read-only linker lookup, suitable for printers and debugger display. + // In particular, do not lazily remove a partially dead function. + public FunNode _link(int fidx) { return _linker.atX(fidx); } + public FunNode _link(TypeFunPtr tfp) { return _link(tfp.fidx()); } + // Return the FunNode from a fidx + public FunNode link( int fidx ) { + FunNode fun =_linker.atX(fidx); + if( fun!=null && fun.isDead() ) { + _linker.setX(fidx,null); fun = null; } + return fun; + } + // Return the for a CompUnit + public FunNode link( CompUnit cu ) { + for( FunNode fun : _linker ) + if( fun!=null && fun._compunit == cu && fun.isClz() ) + return fun; + return null; + } + + // Insert linker mapping from constant function signature to the function + // being called. + public void link(FunNode fun) { + int fidx = fun.sig().fidx(); + _linker.setX(fidx,fun); + } + + // "Linker" mapping from constant TypeFunPtrs to heads of function. These + // TFPs all have exact single fidxs and their return is wiped to BOTTOM (so + // the return is not part of the match). + public final Ary _linker = new Ary<>(FunNode.class); + public final Ary _calls = new Ary<>(CallNode.class); + + // Extern function declarations; input is the FIDX assigned to the name. + final HashMap _externFunc = new HashMap<>(); + public void externFunc(int fidx, String ex) { + assert !_externFunc.containsKey(fidx); + _externFunc.put(fidx,ex); + } + public String externFunc(int fidx) { return _externFunc.get(fidx); } + + public String funcName(int fidx ) { + FunNode fun = _link(fidx); + if( fun!=null ) return fun._name; + return externFunc(fidx); + } + + public boolean owns(FunNode fun) { + return fun._compunit != null && fun._compunit._src != null; + } + + + // --------------------------- + // Parser object + public final Parser P; + + // Parse ASCII text into Sea-of-Nodes IR + public CodeGen parse() { return phase(Phase.Parse, _phase==null, () -> ParseAll.parse(this,_src)); } + + + // --------------------------- + // Iterator peepholes. + public final IterPeeps _iter; + // Stop tracking deps while assertin + public boolean _midAssert; + // Statistics on peepholes + public int _iter_cnt, _iter_nop_cnt; + // Stat gathering + public void iterCnt() { if( !_midAssert ) _iter_cnt++; } + public void iterNop() { if( !_midAssert ) _iter_nop_cnt++; } + + // Pessimistic peepholes after parsing. This lifts loaded and parsed + // types before the optimistic Opto pass resets types and lets them fall. + public CodeGen iter() { return phase(Phase.Iter, _phase==Phase.Parse, () -> Iter.run(this)); } + + // SCCP and numeric-mode resolution; leave cleanup work queued for Iter. + public CodeGen opto() { return phase(Phase.Opto, _phase==Phase.Iter, () -> Opto.opto(this)); } + + public CodeGen iterPostOpto() { return phase(Phase.IterPostOpto, _phase==Phase.Opto, () -> _iter.iterate(this)); } + + public CodeGen finalizeIR() { return phase(Phase.FinalizeIR, _phase==Phase.IterPostOpto, () -> FinalizeIR.run(this)); } + + // Root a surviving function without claiming that it has unknown callers. + void keepInCompUnit(FunNode fun) { + StopCUNode stop = fun._compunit._stop; + if( stop._inputs.find(fun.ret()) == -1 ) + add(stop).addDef(fun.ret()); + } + public N add( N n ) { return _iter.add(n); } + public void addAll( Ary ary ) { _iter.addAll(ary); } + public void addAll( Node n ) { _iter.add(n); _iter.addAll(n._inputs); } + + // --------------------------- + // Last check for bad programs + public CodeGen typeCheck() { return phase(Phase.TypeCheck, _phase==Phase.FinalizeIR, () -> TypeCheck.run(this)); } + + public static TypeStruct requiredStruct(Node n) { + // CNC: remove? cleanup? + if( n instanceof NewNode alloc ) return alloc._ts; + if( n instanceof ConFldOffNode off ) return off._ts; + if( n instanceof FldOffNode off && off.in(0)._type instanceof TypeMemPtr ptr ) return ptr._obj; + return null; + } + + // --------------------------- + // TypeCheck queued surviving CLInits during its existing error walk. + public CodeGen lower() { + return phase(Phase.Lower, _phase==Phase.TypeCheck, () -> _iter.iterate(this)); + } + + // --------------------------- + // Build the loop tree; break never-exit loops + public CodeGen loopTree() { + return phase(Phase.LoopTree, _phase==Phase.Lower, () -> { _start.buildLoopTree(_linker,_stop); FinishCalls.run(this); }); + } + + // --------------------------- + + + // --------------------------- + // Code generation CPU target + public Machine _mach; + // Chosen calling convention (usually either Win64 or SystemV) + public String _callingConv; + public String _cCallingConv; + // Callee save registers + public RegMask _callerSave; + public RegMask _cCallerSave; + + // All returns have the following inputs: + // 0 - ctrl + // 1 - memory + // 2 - varies with returning GPR,FPR + // 3 - RPC + // 4+- Caller save registers + public RegMask[] _retMasks; + // Return Program Counter + public RegMask _rpcMask; + + // Convert to target hardware nodes + public CodeGen instSelect( String cpu, String callingConv ) { return instSelect(cpu,callingConv,PORTS); } + public CodeGen instSelect( String cpu, String callingConv, String base ) { + return InstSelect.select(this,cpu,callingConv,base); + } + + // --------------------------- + + + // --------------------------- + // Control Flow Graph in Reverse Post Order. + public Ary _cfg = new Ary<>(CFGNode.class); + + // Global schedule (code motion) nodes + public CodeGen GCM() { + return phase(Phase.Schedule, (_phase==Phase.LoopTree || _phase==Phase.Select), () -> GlobalCodeMotion.buildCFG(this)); + } + + // --------------------------- + // Local (basic block) scheduler phase, a classic list scheduler + public CodeGen localSched() { return phase(Phase.LocalSched, _phase==Phase.Schedule, () -> ListScheduler.sched(this)); } + + + // --------------------------- + // Register Allocation + public RegAlloc _regAlloc; + public CodeGen regAlloc() { + return phase(Phase.RegAlloc, _phase==Phase.LocalSched, () -> { + _regAlloc = new RegAlloc(this); + _regAlloc.regAlloc(); + }); + } + + // Human-readable register name + public String reg(Node n) { + if( _phase.ordinal() >= Phase.RegAlloc.ordinal() ) { + String s = _regAlloc.reg(n); + if( s!=null ) return s; + } + return "N"+ n._nid; + } + + + // --------------------------- + // Encoding + public Encoding _encoding; + public CodeGen encode() { return phase(Phase.Encoding, _phase==Phase.RegAlloc, () -> _encoding = new Encoding(this).encode()); } + + // --------------------------- + // Exporting to external formats + ElfWriter _elf; + public CodeGen exportELF( boolean inMemory, boolean emitEntrySymbol ) { + return phase(Phase.Export, _phase==Phase.Encoding, () -> { + if( inMemory ) new LinkMem(this).link(_encoding); + else _elf = new ElfWriter(this).export(emitEntrySymbol); + }); + } + + + // --------------------------- + + public boolean _asmLittle=true; + public String asm() { return asm(new SB()).toString(); } + SB asm(SB sb) { + ASMPrinter.print(sb,this); + return sb; + } + + + // Testing shortcuts + public Node ctrl() { return compunit()._stop.ret().ctrl(); } + public Node expr() { return compunit()._stop.ret().expr(); } + public String print() { return compunit()._stop.print(); } + + // Debugging helper + @Override public String toString() { + if( _stop == null ) + return "No StopNode"; + return IRPrinter.prettyPrint(this); + } + + // Debugging helper + public Node f(int idx) { + for( CompUnit cu : _compunits.values() ) { + Node n = (cu._stop == null ? _stop : cu._stop).find(idx); + if( n != null ) return n; + } + return null; + } + + + String printCFG() { + if( _cfg==null ) return "no CFG"; + SB sb = new SB(); + for( CFGNode cfg : _cfg ) { + sb.fix(8,""+(int)cfg._idepth); + IRPrinter.printLine( cfg, sb ); + } + return sb.toString(); + } + + public static void print_as_hex(Encoding enc) { + for (byte b : enc._bits.toByteArray()) { + System.out.print(String.format("%02X", b)); + } + System.out.println(); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java new file mode 100644 index 00000000..c8b0a82c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java @@ -0,0 +1,98 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import static com.seaofnodes.simple.codegen.CodeGen.CODE; + +// A source file and its class. Parent links follow the source directory tree. +public class CompUnit { + public final CompUnit _par; // Parent, e.g. A/B + final String _fname; // Full slashed file name starting from module root, e.g. A/B/C + final public String _cname; // Full dotted class name starting from module root, e.g. A.B.C + final String _name; // Base name, e.g. C + final File _smp; // Simple source file, e.g. module_root/A/B/C.smp + final public String _src; // Source code, from file or test case + + public String sourceFile() { + return _smp!=null ? _smp.getAbsolutePath() : null; + } + + public TypeStruct _clz; // The one TypeStruct being published + + // _deps records units required for types, code, or class references, + // without requiring their initializers to run. These source + // dependencies may form a cycle. + Ary _deps; + + // Per-Compilation-Unit Start/StopNodes, keeping alive all exported Nodes. + // Null means no code loaded (yet). + public StartCUNode _start; + public StopCUNode _stop ; + + + // Discovered source file. Source is loaded if required to parse, and null + // otherwise. + CompUnit( CompUnit par, String fname, String cname, String name ) throws IOException { + _par = par; + _fname= fname; + _cname= cname; + _name = name; + _smp = source(fname); + + _src = _smp.exists() ? Files.readString(_smp.toPath()) : null; + } + + // The bundled library is ordinary source. A local sys tree can replace it; + // this chapter never loads Simple code from an object file. + static File source(String fname) { + File file=new File(CODE._modDir,fname+".smp"); + if( file.exists() || !(fname.equals("sys") || fname.startsWith("sys/")) ) return file; + return new File("src/main/smp",fname+".smp"); + } + + // Test case. No source file. Source code passed in as a String. Fake + // class name. + CompUnit( String fname, String src ) { + _par = null; + _name = fname; + _fname= fname; + _cname= fname; + _smp = null; + _src = src; + } + + // Record source discovery without adding an initialization edge. + void addDep(CompUnit dep) { + if( _deps == null ) _deps = new Ary<>(CompUnit.class); + _deps.add(dep); + } + + public void addFun(CodeGen code, FunNode fun) { + assert fun.in(1)==_start; + _stop.addDef(fun.ret()); + code.add(_stop); + _stop.addDep(fun); + } + + void setStart(CodeGen code) { + _stop = new StopCUNode().init(); + _start = new StartCUNode(code._start,_stop, Type.BOTTOM, _fname).init(); + code._stop.addDef(_stop); + } + + @Override public String toString() { + String x = "{"+_cname; + if( _smp!=null && _smp.exists()) x += ",hasSMP"; + if( _src!=null ) x += ",hasSrc"; + x += "}"; + return x; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java new file mode 100644 index 00000000..c52209dd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java @@ -0,0 +1,460 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.BAOS; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; + +public class ElfWriter { + + final CodeGen _code; + Ary
_sections; + StringSection _strtab; + + ElfWriter( CodeGen code ) { + _code = code; + } + + // ------------------------------------------------------------------------ + abstract public class Section { + static final int SHT_NULL = 0; + static final int SHT_PROGBITS= 1; + static final int SHT_SYMTAB = 2; + static final int SHT_STRTAB = 3; + static final int SHT_RELA = 4; + + final String _name; + final int _type, _entsize, _index, _name_pos, _flags; + int _link, _info; + int _file_offset; + + Section(String name, int type, int entsize, int flags) { + _name = name; + _type = type; + _entsize = entsize; + _flags = flags; + _index = _sections._len; + _sections.push(this); + _name_pos = name.isEmpty() + ? 0 // Null section + : _strtab == null + ? 1 // StringSection + // All the other sections + : _strtab.writeCString(name); + } + + void writeHeader(ByteBuffer out) { + out.putInt(_name_pos); // name + out.putInt(_type); // type + out.putLong(_flags); // flags + out.putLong(0); // addr + out.putLong(_file_offset); // offset + out.putLong(size()); // size + out.putInt(_link); // link + out.putInt(_info); // info + out.putLong(16); // addralign + out.putLong(_entsize); // Fixed-size element size + } + + abstract void write(ByteBuffer out); + abstract int size(); + } + + // ------------------------------------------------------------------------ + // Generic data sections; carries about a BAOS for bits + public class DataSection extends Section { + static final int SHF_WRITE = 1; + static final int SHF_ALLOC = 2; + static final int SHF_EXECINSTR = 4; + static final int SHF_INFO_LINK = 64; + + final BAOS _contents; + + DataSection( String name, int type, int flags, int entsize ) { this(name, type, flags, entsize, new BAOS() ); } + DataSection( String name, int type, BAOS contents, int flags ) { this(name, type, flags, 0, contents ); } + private DataSection( String name, int type, int flags, int entsize, BAOS contents ) { + super(name, type, entsize, flags); + _contents = contents; + } + + @Override void write(ByteBuffer out) { out.put(_contents.toByteArray()); } + @Override int size() { return _contents.size(); } + public void write2( int op ) { + _contents.write(op ); + _contents.write(op>> 8); + } + public void write4( int op ) { + _contents.write(op ); + _contents.write(op>> 8); + _contents.write(op>>16); + _contents.write(op>>24); + } + public void write8( long i64 ) { + write4( (int) i64 ); + write4( (int)(i64>>32)); + } + + } + + // ------------------------------------------------------------------------ + // Section to collect and de-dup strings + public class StringSection extends DataSection { + final HashMap _dedup = new HashMap<>(); + + StringSection() { + super(".strtab",SHT_STRTAB, 0, 0 ); + int emptyoff = writeCString(""); // Always empty string at offset 0 + assert emptyoff == 0; + int namepos = writeCString(".strtab"); + assert namepos == _name_pos; + } + + public int writeCString(String name) { + if( name==null ) return 0; // Anonymous functions have no name + Integer ii = _dedup.get(name); + if( ii != null ) return ii; + // place name in the string table + int pos = _contents.size(); + _dedup.put(name,pos); + byte[] name_bytes = name.getBytes(); + _contents.write(name_bytes); + // the strings are null terminated + _contents.write(0); + return pos; + } + } + + // ------------------------------------------------------------------------ + // The relocations; 3 words per relocation + public class ReloSection extends DataSection { + ReloSection( int info ) { + this(".rela.text",info); + } + ReloSection(String name,int info) { + super(name, SHT_RELA, SHF_INFO_LINK, 24/*entsize*/); + _link = 2; + _info = info; + } + + void writeReloOffPlus(int offset, int symidx, byte relo) { + writeRelo(offset,symidx,relo,-4); + } + void writeRelo(int offset,int symidx,byte relo,long addend) { + assert symidx > 0; + write8(offset); + write8(((long)symidx << 32L) | relo); + write8(addend); + } + } + + + // ------------------------------------------------------------------------ + // Symbols section; emit the symbols into the data section directly + public class SymbolSection extends DataSection { + public static final int SYMBOL_SIZE = 4+1+1+2+8+8; + final BAOS _globals; + final int _nlocals; + + SymbolSection(int nlocals) { + super(".symtab", SHT_SYMTAB, 0/*flags*/, SYMBOL_SIZE); + _globals = new BAOS(); + _nlocals = nlocals; + } + + // Global symbol index + int gidx() { return _nlocals+(_globals.size()/SYMBOL_SIZE); } + + public int symbol( String name, int shndx, int bind, int type, long value, long size ) { + int gidx = gidx(); // Global index before I write to _globals + BAOS baos = bind==SYM_BIND_LOCAL ? _contents : _globals; + write4(baos,_strtab.writeCString(name)); + int info = (bind << 4) | (type & 0xF); + baos.write(info); + baos.write(0); // other/visibility + write2(baos,shndx); // section index + write8(baos,value); + write8(baos,size); + return bind==SYM_BIND_LOCAL ? -99 : gidx; + } + + public int symbol( String name, int shndx, int bind, int type ) { + return symbol(name,shndx,bind,type,0,0); + } + + // creates function and stores where it starts. + // Filters to just this class.obj file. + void encodeFunctions(StopNode stop, Encoding enc, int text_idx, boolean emitEntrySymbol) { + for( Node stopcu : stop._inputs ) { + for( Node ret : stopcu._inputs ) { + FunNode fun = ((ReturnNode)ret).fun(); + if( !_code.owns(fun) ) + continue; + int end = enc.opStart(fun.ret()) + enc.opLen(fun.ret()); + long value = enc.opStart(fun); + long size = end - value; + if( fun._name != null ) { // Anonymous functions have no name + // The C runtime owns `main` and adapts argc/argv into + // a normal Simple array. Keep the Simple function on + // its native ABI under a private entry symbol. + String name = emitEntrySymbol && "main".equals(fun._name) + ? "simple_main" + : fun._name; + symbol(name, text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, value, size); + } + } + } + } + + // Insert a default-main for top-level compilations, if the top-level + // does not declare a main. + void symbolEntryClinit(int text_idx, Encoding enc) { + FunNode clinit = null; + String entryName = _code.entryClinitName(); + for( FunNode fun : _code._linker ) + if( fun != null && _code.owns(fun) ) { + if( "main".equals(fun._name) ) + return; // encodeFunctions exported this as simple_main + if( entryName.equals(fun._name) ) + clinit = fun; // Keep top-level entry, if we don't find an explicit main + } + // No top-level main, so the top-level clinit becomes a default main + if( clinit != null ) { + assert enc.opStart(clinit) == 0; + symbol("simple_main",text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, enc.opStart(clinit), 0); + return; + } + throw new IllegalStateException("Missing entry "+entryName); + } + + static void write2( BAOS bits, int op ) { + bits.write(op ); + bits.write(op>> 8); + } + static void write4( BAOS bits, int op ) { + bits.write(op ); + bits.write(op>> 8); + bits.write(op>>16); + bits.write(op>>24); + } + static void write8( BAOS bits, long i64 ) { + write4(bits, (int) i64 ); + write4(bits, (int)(i64>>32)); + } + + @Override + void writeHeader(ByteBuffer out) { + // points to string table section + _link = _strtab._index; + _info = _nlocals; + super.writeHeader(out); + } + + @Override void write(ByteBuffer out) { + out.put(_contents.toByteArray()); + out.put(_globals .toByteArray()); + } + @Override int size() { + return _contents.size() + _globals.size(); + } + } + + public static final int SYM_BIND_LOCAL = 0; + public static final int SYM_BIND_GLOBAL = 1; + public static final int SYM_BIND_WEAK = 2; + + public static final int SYM_TYPE_NOTYPE = 0; + public static final int SYM_TYPE_OBJECT = 1; + public static final int SYM_TYPE_FUNC = 2; + public static final int SYM_TYPE_SECTION = 3; + + public static final int SECTION_HDR_SIZE = 64; + + // ------------------------------------------------------------------------ + + + // ------------------------------------------------------------------------ + + // Native code can live above 4G, outside our four-byte function pointers. + // Give address-taken natives a low entry which tail-jumps through the PLT. + // A weak, stable name makes references from separate objects agree on one + // entry. Do not patch references straight to the local copy: that would + // bypass the linker's choice and break pointer equality across objects. + private int nativeEntry(String name, HashMap entries, + DataSection text, SymbolSection symbols, ReloSection relos) { + Integer old = entries.get(name); + if( old!=null ) return old; + int offset = text.size(); + text._contents.write(0xE9); // jmp rel32; preserve arguments and return address + text.write4(0); + int target = symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_FUNC); + relos.writeReloOffPlus(offset+1,target,(byte)4/*PLT32*/); + int entry = symbols.symbol("__simple_native$"+name,text._index, + SYM_BIND_WEAK,SYM_TYPE_FUNC,offset,5); + entries.put(name,entry); + return entry; + } + + public ElfWriter export(boolean emitEntrySymbol) { + // Sections are created in the order they are emitted. + _sections = new Ary<>(Section.class); + + // Null section + Section nullSection = new DataSection("",Section.SHT_NULL,0,0); + + // String section next; written out early and available to record/dedup + // strings for all remaining sections + _strtab = new StringSection(); + + // place all the symbols + int nlocals = 1/*null*/+9/*number of sections*/; + SymbolSection symbols = new SymbolSection(nlocals); + + // we've already constructed these entire sections in the encoding phase + Encoding enc = _code._encoding; + // Append ELF-only thunks to a copy. Export must not change instruction + // offsets or the image used by the in-memory linker and emulators. + BAOS textBits = new BAOS(enc._bits.toByteArray(),enc._bits.size()); + DataSection text = new DataSection(".text" , Section.SHT_PROGBITS, textBits, DataSection.SHF_ALLOC | DataSection.SHF_WRITE | DataSection.SHF_EXECINSTR ); + // Constant pool + DataSection rodata = new DataSection(".rodata", Section.SHT_PROGBITS, enc._cpool, DataSection.SHF_ALLOC ); + // Static/global pool + DataSection rwdata = new DataSection(".data" , Section.SHT_PROGBITS, enc._sdata, DataSection.SHF_ALLOC | DataSection.SHF_WRITE ); + + + // populate function symbols + symbols.encodeFunctions(_code._stop, _code._encoding, text._index, emitEntrySymbol); + // Export the source compilation unit as the C runtime "main". + // Encoding keeps this function at text offset zero. + if( emitEntrySymbol ) + symbols.symbolEntryClinit(text._index,enc); + + // create external .text relocations + ReloSection relocations = new ReloSection(text._index); + HashMap nativeEntries = new HashMap<>(); + + for( Node n : enc._externals.keySet()) { + String extern = enc._externals.get(n); + boolean nativeAddress = n instanceof FunPtrNode && + _code.externFunc(((TypeFunPtr)n._type).fidx())!=null; + int symidx = nativeAddress + ? nativeEntry(extern,nativeEntries,text,symbols,relocations) + : symbols.symbol(extern,0,SYM_BIND_GLOBAL,SYM_TYPE_FUNC); + int offset = enc.opStart(n) + enc.opLen(n) - 4; + // Address materialization and static fields name the same thunk; + // direct calls still go straight to the native symbol's PLT entry. + relocations.writeReloOffPlus(offset,symidx,(byte)(nativeAddress ? 2/*PC32*/ : 4/*PLT32*/)); + } + + // Native ELF output is AMD64. Data addresses use PC32, not PLT calls. + for( Node n : enc._externData.keySet() ) { + String extern = enc._externData.get(n); + int symidx = symbols.symbol(extern, 0, SYM_BIND_GLOBAL, SYM_TYPE_OBJECT); + int offset = enc.opStart(n) + enc.opLen(n) - 4; + relocations.writeReloOffPlus(offset, symidx, (byte)2/*PC32*/); + } + + // Stable function identities are also addressable from data, including + // anonymous functions which have no source-level exported name. + HashMap dataSymbols=new HashMap<>(); + for( FunNode fun : _code._linker ) + if( fun!=null && !fun.isDead() && _code.owns(fun) && enc.opStart(fun)>=0 ) { + String name=_code._fidxs.symbol(fun.sig().fidx()); + dataSymbols.put(name,symbols.symbol(name,text._index,SYM_BIND_GLOBAL,SYM_TYPE_FUNC,enc.opStart(fun),0)); + } + for( StaticData.ObjectData obj : enc._data.objects.values() ) { + int section=obj.external ? 0 : obj.readOnly ? rodata._index : rwdata._index; + dataSymbols.put(obj.symbol,symbols.symbol(obj.symbol,section,SYM_BIND_GLOBAL,SYM_TYPE_OBJECT, + obj.external ? 0 : obj.offset,obj.external ? 0 : obj.size)); + } + for( Encoding.Relo relo : enc._bigCons.values() ) { + StaticData.ObjectData obj=enc._data.object(relo._t); + relocations.writeReloOffPlus(relo._opStart+relo._off,dataSymbols.get(obj.symbol),relo._elf); + } + ReloSection roRelos=new ReloSection(".rela.rodata",rodata._index); + ReloSection rwRelos=new ReloSection(".rela.data",rwdata._index); + for( StaticData.Relocation relo : enc._data.relocations ) { + String name=enc._data.targetSymbol(relo.value()); + String nativeName=relo.value() instanceof TypeFunPtr fp ? _code.externFunc(fp.fidx()) : null; + Integer sym=nativeName==null ? dataSymbols.get(name) + : nativeEntry(nativeName,nativeEntries,text,symbols,relocations); + if( sym==null ) { + sym=symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_NOTYPE); + dataSymbols.put(name,sym); + } + // AMD64 absolute 32/64-bit addresses; linker rejects overflow. + byte kind=(byte)(relo.logSize()==2 ? 10 : 1); + (relo.object().readOnly ? roRelos : rwRelos).writeRelo(relo.object().offset+relo.offset(),sym,kind,0); + } + + // Create a "Simple" section for Simple types and ir + + // Write the local section header symbols + assert nlocals == 1+_sections._len; + symbols.symbol("",0,0,0); // Null symbol at index 0 + for( Section s : _sections ) + symbols.symbol(s._name, s._index, SYM_BIND_LOCAL, SYM_TYPE_SECTION, 0, s.size()); + + int size = 64; // ELF header size + // size of all section data + for( Section s : _sections ) { + s._file_offset = size; + size += s.size(); + } + // headers go at the very end + int shdr_offset = size; + size += SECTION_HDR_SIZE*_sections.len(); + + ByteBuffer out = ByteBuffer.allocate(size); + out.order(ByteOrder.LITTLE_ENDIAN); + + // ELF header + out.put(new byte[]{ 0x7F, 'E', 'L', 'F', 2, 1, 1, 0 }); + out.put(new byte[8]); + out.putShort((short)1 /* ET_REL */); // type + out.putShort((short)62 /* AMD64 */); // machine + out.putInt(1); // version + out.putLong(0); // entry + out.putLong(0); // phoff + out.putLong(shdr_offset); // shoff + out.putInt(0); // flags + out.putShort((short)64); // ehsize + out.putShort((short)0); // phentsize + out.putShort((short)0); // phnum + out.putShort((short)64); // shentsize + out.putShort((short) _sections.len()); // shnum + out.putShort((short) _strtab._index); // shstrndx + + // raw section data + for( Section s : _sections ) + s.write(out); + + // Section headers + for( Section s : _sections ) + s.writeHeader(out); + + assert out.position() == size; + + String objName = _code._buildDir + "/" + _code._srcName + ".o"; + File file = new File(objName); + if( file.getParentFile()!=null ) + file.getParentFile().mkdirs(); + try { + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); + bos.write(out.array()); + bos.close(); + } catch( IOException ioe ) { + // Caller does not want to deal, but it is a failed compilation in any case + throw new RuntimeException(ioe); + } + // Reset for next ELF + _sections = null; + _strtab = null; + return this; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Encoding.java new file mode 100644 index 00000000..06bf14f5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Encoding.java @@ -0,0 +1,570 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.print.IRPrinter; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.*; + + +/** + * Instruction encodings + *

+ * This class holds the encoding bits, plus the relocation information needed + * to move the code to a non-zero offset, or write to an external file (ELF). + *

+ * There are also a bunch of generic utilities for managing bits and bytes + * common in all encodings. + */ +public class Encoding implements com.seaofnodes.isa.CodeSink { + + // Top-level program graph structure + public final CodeGen _code; + + // CFG Nodes in RPO order, per-encoding. + public Ary _cfg; + + // Instruction bytes. The registers are encoded already. Relocatable data + // is in a fixed format depending on the kind of relocation. + + // - RIP-relative offsets into the same chunk are just encoded with their + // chunk relative offsets; no relocation info is required. + + // - Fixed address targets in the same encoding will be correct for a zero + // base; moving the entire encoding requires adding the new base address. + + // - RIP-relative to external chunks have a zero offset; the matching + // relocation info will be used to patch the correct value. + + public final BAOS _bits = new BAOS(); // Instructions / encodings + public final BAOS _cpool = new BAOS(); // Constant r/o pool + public final BAOS _sdata = new BAOS(); // Static r/w pool + public final HashMap _bigCons = new HashMap<>(); + public final StaticData _data; + public final HashMap _externalAddresses = new HashMap<>(); + + // Big Constant relocation info. + public static class Relo { + public final Node _op; + public final Type _t; // Constant type + public final int _align; // Log alignment, captured for diagnostic display + public final int _structSize; // Capture layout during encoding, not printing + public final byte _off; // Offset from start of opcode + public final byte _elf; // ELF relocation type, e.g. 2/PC32 + public int _target; // Where constant is finally placed + public int _opStart; // Opcode start + Relo( Node op, Type t, byte off, byte elf ) { + _op=op; _t=t; _off=off; _elf=elf; + _align = t.alignment(); + _structSize = t instanceof TypeStruct ts ? ts.size() : 0; + _readOnly = readOnly(t); + } + public boolean readOnly() { return _readOnly; } + private final boolean _readOnly; + private static boolean readOnly(Type t) { + // Class initializers still write their fields, even when every + // final value is constant. Their storage must remain writable. + return !(t instanceof TypeStruct ts) || + (!Parser.startsClzPrefix(ts._name) && ts.isConstant()); + } + } + + Encoding( CodeGen code ) { + _code = code; + _data = new StaticData(this); + } + + // Shortcut to the defining register + public short reg(Node n) { + return _code._regAlloc.regnum(n); + } + + public int read1(int idx) { + byte[] buf = _bits.buf(); + return (buf[idx]&0xFF); + } + public int read2(int idx) { + byte[] buf = _bits.buf(); + return (buf[idx]&0xFF) | (buf[idx+1]&0xFF) <<8; + } + public int read4(int idx) { + byte[] buf = _bits.buf(); + return (buf[idx]&0xFF) | (buf[idx+1]&0xFF) <<8 | (buf[idx+2]&0xFF)<<16 | (buf[idx+3]&0xFF)<<24; + } + public long read8(int idx) { + return (read4(idx) & 0xFFFFFFFFL) | read4(idx+4); + } + + // This buffer is invalid/moving until after all encodings are written + public byte[] bits() { return _bits.buf(); } + + // 4 byte little-endian write + public void patch4( int idx, int val ) { + byte[] buf = _bits.buf(); + buf[idx ] = (byte)(val ); + buf[idx+1] = (byte)(val>> 8); + buf[idx+2] = (byte)(val>>16); + buf[idx+3] = (byte)(val>>24); + } + + static void padN(int n, BAOS bits) { + while( (bits.size()+n-1 & -n) > bits.size() ) + bits.write(0); + } + + // Convenience for writing log-N + static void addN( int log, long x, BAOS bits ) { + for( int i=0; i < 1<>= 8; + } + } + + public Encoding add1( int op ) { addN(0,op,_bits); return this; } + public Encoding add2( int op ) { addN(1,op,_bits); return this; } + public Encoding add4( int op ) { addN(2,op,_bits); return this; } + public Encoding add8(long op ) { addN(3,op,_bits); return this; } + + private final static int SENTINEL = -1; + private final IntHashMap _opStart = new IntHashMap(SENTINEL); + private final IntHashMap _opLen = new IntHashMap(SENTINEL); + + + // Read op start for n + public int opStart(Node n) { return _opStart.get(n._nid); } + // Read op length for n + public byte opLen (Node n) { return (byte)_opLen.get(n._nid); } + + // Set op start for n + public void opStart(Node n, int off) { + assert off != SENTINEL; + _opStart.put(n._nid,off); + } + + // Set op start for n + public void opStartAdd(Node n, int off) { + if( off==0 ) return; + int old = _opStart.get(n._nid); + assert old != SENTINEL && old+off != SENTINEL; + _opStart.put(n._nid,old+off); + } + + // Set op length for n + public void opLen(Node n, byte len) { + assert len != SENTINEL; + _opLen.put(n._nid,len); + } + + // Nodes need "relocation" patching; things done after code is placed. + // Record src and dst Nodes. + private final HashMap _internals = new HashMap<>(); + // Source is a Call, destination in the Fun. + public Encoding relo( CallNode call ) { + FunNode fun = _code.link(call.tfp()); + if( fun != null && !_code.owns(fun) ) + _externals.put(call,fun._name); + else + _internals.put(call,fun); + return this; + } + // Code address as a constant + public Encoding relo( FunPtrNode fptr ) { + TypeFunPtr tfp = (TypeFunPtr)fptr._type; + // Native functions have no local FunNode, but are still callable. + // Do not treat their addresses as identities of deleted functions. + String nativeName = _code.externFunc(tfp.fidx()); + if( nativeName!=null ) return external(fptr,nativeName); + FunNode fun = _code.link(tfp); + if( fun != null && !_code.owns(fun) ) + _externals.put(fptr,fun._name); + else + _internals.put(fptr,fun); + return this; + } + // Local jump + public void jump( CFGNode jmp, CFGNode dst ) { + _internals.put(jmp,dst.uctrlSkipEmpty()); + } + + // External references only located by Strings a link-time + public final HashMap _externals = new HashMap<>(); + public Encoding external( Node n, String extern ) { + _externals.put(n,extern); + return this; + } + + public final HashMap _externData = new HashMap<>(); + public void externalData(Node n, String name) { _externData.put(n,name); } + + // Store t as a 32/64 bit constant in the code space; generate RIP-relative + // addressing to load it. Type is stored in either the .rodata or .data. + public void largeConstant( Node relo, Type t, int off, int elf ) { + // TODO: Any-old struct in the cpool + assert t.isConstant() || (t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name)); + assert (byte)off == off; + assert (byte)elf == elf; + _bigCons.put(relo,new Relo(relo,t,(byte)off,(byte)elf)); + } + + // -------------------------------------------------- + Encoding encode( ) { + // Basic block layout: negate branches to keep blocks in-order; insert + // unconditional jumps. Attempt to keep backwards branches taken, + // forwards not-taken (this is the default prediction on most + // hardware). Layout is still Reverse Post Order but with more + // restrictions. + basicBlockLayout( ); + + // Write encoding bits in order into a big byte array. + // Record opcode start and length. + writeEncodings(); + + // Short-form RIP-relative support: replace long encodings with short + // encodings and compact the code, changing all the offsets. + compactShortForm(); + + // Place pool objects and record relocations within their contents. + _data.write(); + + // Patch RIP-relative and local encodings now. + patchLocalRelocations(); + + return this; + } + + // -------------------------------------------------- + // Basic block layout: negate branches to keep blocks in-order; insert + // unconditional jumps. Attempt to keep backwards branches taken, forwards + // not-taken (this is the default prediction on most hardware). Layout is + // still Reverse Post Order but with more restrictions. + private void basicBlockLayout( ) { + IdentityHashMap> rpos = new IdentityHashMap<>(); + _cfg = new Ary<>(CFGNode.class); + rpos.put(_code._start.loop(),_cfg); + BitSet visit = _code.visit(); + + // Do them all except the s + Ary clinits = new Ary<>(FunNode.class); + FunNode entry = null; + String entryName = _code.entryClinitName(); + for( FunNode fun : _code._linker ) { + if( fun != null && !fun.isDead() && _code.owns(fun) ) { + if( fun.isClz() ) { + if( fun._name.equals(entryName) ) + entry = fun; + else + clinits.add(fun); + } else { + int x = _cfg._len; + _rpo_cfg(fun,visit,rpos); + assert _cfg.at(x) instanceof ReturnNode; + } + } + } + // Now the s last, so when reversed they become first. The + // program entry is last of all, so it lands at offset zero. + for( FunNode clinit : clinits ) { + int x = _cfg._len; + _rpo_cfg(clinit,visit,rpos); + assert _cfg.at(x) instanceof ReturnNode; + } + if( entry != null ) { + int x = _cfg._len; + _rpo_cfg(entry,visit,rpos); + assert _cfg.at(x) instanceof ReturnNode; + } + + + // Reverse in-place + for( int i=0; i< _cfg.size()>>1; i++ ) + _cfg.swap(i,_cfg.size()-1-i); + visit.clear(); + } + + + private void _rpo_cfg(CFGNode bb, BitSet visit, IdentityHashMap> rpos ) { + if( bb==null || visit.get(bb._nid) ) return; + visit.set(bb._nid); + CFGNode next = bb.uctrl(); + + // Loops run an inner "rpo_cfg", then append the entire loop body in + // place. This keeps loop bodies completely contained, although if + // Some Day Later we have real profile data we ought to arrange the + // branch orderings based on frequency. + if( bb instanceof LoopNode loop ) { + Ary body = new Ary<>(CFGNode.class); // Private RPO for the loop + rpos.put(loop.loop(),body); // Find it via loop tree + _rpo_cfg(next,visit,rpos); // RPO the loop + body.add(loop); // Include loop last (first in RPO) + Ary outer = rpos.get(loop.cfg(1).loop()); + outer.addAll(body); // Append to original CFG + return; + } + + // IfNodes visit 2 sides, and may choose to reorder them in the RPO + if( bb instanceof IfNode iff ) { + // Pick out the T/F projections + CProjNode t = iff.cproj(0); + CProjNode f = iff.cproj(1); + // Invert the branch or not + if( shouldInvert(t,f,iff.loopDepth()) ) { + iff.negate(); + t.invert(); + f.invert(); + CProjNode tmp=f; f=t; t=tmp; // Swap t/f + } + + // Whichever side is visited first becomes last in the RPO. With + // no loops, visit the False side last (so True side first) so that + // when the False RPO visit returns, the IF is immediately next. + // When the RPO is reversed, the fall-through path will always be + // following the IF. + _rpo_cfg(t,visit,rpos); // True side first + next = f; // False side last + } + + // If the *next* BB has already been visited, and we are not already a + // jump, we may need an unconditional forwards jump here + Ary rpo = rpos.get(bb.loop()); + if( next!=null && visit.get(next._nid) && !(bb instanceof IfNode) ) { + boolean needJump = next instanceof LoopNode + // Empty blocks from an IF will invert the IF and backwards + // branch to the loop head. If not empty, or not an IF + // will need a jump. + ? (bb.nOuts()>1 || !(bb.cfg0() instanceof IfNode) ) + // Forwards jump. If all the blocks, in RPO order, to our + // target are empty, we will fall in and not need a jump. + : !backwardsEmptyScan(rpo,next); + if( needJump ) { + CFGNode jmp = _code._mach.jump(); + jmp._ltree = bb._ltree; + jmp.setDefX(0,bb); + next.setDef(next._inputs.find(bb),jmp); + rpo.add(jmp); + } + } + + _rpo_cfg(next,visit,rpos); + rpo.add(bb); + } + + // Should this test be inverted? + private static boolean shouldInvert(CFGNode t, CFGNode f, int bld) { + int tld = t.loopDepth(), fld = f.loopDepth(); + // These next two are symmetric and can happen in any order; if `tld < + // bld` is true, then `fld < bld` must be false, or else both directions + // exit the loop... and the IF test would not be in the loop. + + // true to exit a loop usually (and false falls into Yet Another Loop + // Block), invert if false is taking an empty backwards branch. + if( tld < bld ) return forwardsEmptyScan(f,bld); + // false to exit a loop, normally invert to true (except empty back) + if( fld < bld ) return !forwardsEmptyScan(t,bld); + + // Not exiting the loop; staying at this depth (or going deeper loop). + + // Jump/true to an empty backwards block + if( forwardsEmptyScan(t,bld) ) return false; + if( forwardsEmptyScan(f,bld) ) return true ; + + // Fall/false into a full block, Jump/true to an empty block. + if( f.nOuts()>1 && t.nOuts()==1 ) return false; + if( t.nOuts()>1 && f.nOuts()==1 ) return true ; + // Jump to a merge point, assuming other things are jumping there as well + if( f.out(0) instanceof RegionNode ) return true ; + if( t.out(0) instanceof RegionNode ) return false; + // Everything else equal, use pre-order + return t._pre > f._pre; + } + + // Do we continue forwards from 'c' to the Loop back edge, without doing + // anything else? Then this branch can directly jump to the loop head. + private static boolean forwardsEmptyScan( CFGNode c, int bld ) { + if( c.nOuts()!=1 || c.loopDepth()!=bld ) return false; + return c.uctrl() instanceof RegionNode cfg && + ((cfg instanceof LoopNode && cfg._ltree==c._ltree) || forwardsEmptyScan(cfg,bld)); + } + + // Is the CFG from "next" to the end empty? This means jumping to "next" + // will naturally fall into the end. + private static boolean backwardsEmptyScan(Ary rpo, CFGNode next) { + for( int i=rpo._len-1; rpo.at(i)!=next; i-- ) + if( rpo.at(i).nOuts()!=1 ) + return false; + return true; + } + + // -------------------------------------------------- + // Write encoding bits in order into a big byte array. + // Record opcode start and length. + + // Current function is used by the spill-op encodings to query the stack + // frame layout and get the spill offsets. + private void writeEncodings() { + for( CFGNode bb : _cfg ) { + if( !(bb instanceof MachNode mach0) ) + opStart(bb, _bits.size()); + else if( bb instanceof FunNode ) { + opStart(bb, _bits.size()); + mach0.encoding( this ); + opLen(bb, (byte) (_bits.size() - opStart(bb))); + } + for( Node n : bb._outputs ) { + if( n instanceof MachNode mach && !(n instanceof FunNode) && + // FunPtrs are encoding in the block of their control, not + // the block of the ReturnNode input - which is also a CFG. + !(n instanceof FunPtrNode && bb instanceof ReturnNode) ) { + opStart(n, _bits.size()); + mach.encoding( this ); + opLen(n, (byte) (_bits.size() - opStart(n))); + } + } + } + } + + // -------------------------------------------------- + // Short-form RIP-relative support: replace short encodings with long + // encodings and expand the code, changing all the offsets. + private void compactShortForm() { + int len = _cfg._len; + int[] oldStarts = new int[len]; + for( int i=0; i 0 ) { // If no short-form ops, nothing to do here + int end = _bits.size(); + byte[] bits = new byte[end+grow]; + for( int i=len-1; i>=0; i-- ) { + int start = oldStarts[i]; + if( start==0 && i>1 ) continue; + int oldStart = oldStarts[i]; + int newStart = opStart(_cfg.at(i)); + System.arraycopy(_bits.buf(),oldStart,bits,newStart,end-start); + end = start; + } + _bits.set(bits,bits.length); + } + + } + + + // -------------------------------------------------- + // Patch local encodings now + void patchLocalRelocations() { + // Walk the local code-address relocations + for( Node src : _internals.keySet() ) { + int start = opStart(src); + Node dst = _internals.get(src); + // If function is entirely dead, only the function pointer remains + // and, it can only be used to test against zero or equals to + // another function pointer... i.e., there Is No Code Here. + int target = dst == null ? start : opStart(dst); + ((RIPRelSize)src).patch(this, start, opLen(src), target - start); + } + } + + // Emit a single scalar as bits + static void addN( int log, Type t, BAOS bits ) { + long x = switch( t ) { + case TypeInteger ti -> ti.value(); + case TypeFloat tf -> log==3 + ? Double.doubleToRawLongBits( tf.value()) + : Float.floatToRawIntBits((float)tf.value()); + case TypeFunPtr tfp -> 0; // These need to be relocated + case TypeMemPtr tmp -> 0; // These need to be relocated + default -> { if( t==Type.NIL ) yield 0; else throw Utils.TODO(); } + }; + addN(log,x,bits); + } + + // A series of libc/external calls that Simple can link against in a JIT. + // Since no runtime in the JVM process, using magic numbers for the CPU + // emulators to pick up on. + public static int SENTINEL_CALLOC = -4; + public static int SENTINEL_WRITE = -8; + + int externalAddress(String name) { + Integer address=_externalAddresses.get(name); + if( address!=null ) return address; + return switch(name) { + case "calloc" -> SENTINEL_CALLOC; + case "write" -> SENTINEL_WRITE; + default -> throw new IllegalArgumentException("Unresolved external function '"+name+"'"); + }; + } + + void patchGlobalRelocations() { + for( Node src : _externData.keySet() ) { + String name = _externData.get(src); + Integer target = _code._externDataAddresses.get(name); + if( target==null ) throw new IllegalArgumentException("Unresolved C data symbol '"+name+"'"); + int start = opStart(src); + ((RIPRelSize)src).patch(this,start,opLen(src),target-start); + } + for( Node src : _externals.keySet() ) { + int start = opStart(src); + String dst = _externals.get(src); + int target = externalAddress(dst); + ((RIPRelSize)src).patch(this, start, opLen(src), target - start); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java new file mode 100644 index 00000000..923486c6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import java.util.Arrays; + +// Comnpute field layouts only after SCCP and its peepholes converge. +// Keep surviving functions rooted without introducing unknown callers. +abstract class FinalizeIR { + static void run(CodeGen code) { + // Compute field layouts and replace symbolic field offsets by constants. + for( int i=0; i ((((ReturnNode)x).fun().isClz() ? -1 : x._nid) - + (((ReturnNode)y).fun().isClz() ? -1 : y._nid) )); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java new file mode 100644 index 00000000..7ba696d3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java @@ -0,0 +1,57 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; + +// Detach analysis call edges after building the loop tree. Every live body +// belongs to this invocation; preserve it for scheduling and emission. +abstract class FinishCalls { + + + static void run(CodeGen code) { + + // The remaining passes assume all calls are unlinked; i.e. we are throwing + // away the Call Graph here. Functions only reachable from internal calls + // need to be re-hooked to stop/start less they go dead. + for( FunNode fun : code._linker ) { + // Already linked to start, not going dead + if( fun==null || fun.isDead() ) + continue; + + // Insert the post-Opto code-generation hook. Unlike an Opto-time + // Start input, this no longer carries unknown-caller semantics. + ReturnNode ret = fun.ret(); + StartNode start = fun._compunit._start; + assert start!=null; + if( fun.nIns() <= 1 || fun.in(1) != start ) { + fun.insertDef(1,start); + for( Node use : fun._outputs ) + if( use instanceof ParmNode parm ) + parm.insertDef(1,ConstantNode.raw(parm._type)); + if( fun._compunit._stop._inputs.find(ret) == -1 ) + fun._compunit._stop.addDef(ret); + } + // Unlink from Call + for( int i=2; i { + if( n instanceof CallEndNode cend ) + for( int i=1; i rpo = new Ary<>(CFGNode.class); + _rpo_cfg(null, code._start, code.visit(), rpo); + // Reverse in-place + for( int i=0; i< rpo.size()>>1; i++ ) + rpo.swap(i,rpo.size()-1-i); + // Set global CFG + code._cfg = rpo; + + schedEarly(code); + + // Break up shared global constants by functions + breakUpGlobalConstants(code._start); + + code._visit.clear(); + schedLate(code); + } + + // Post-Order of CFG + private static void _rpo_cfg(CFGNode def, Node use, BitSet visit, Ary rpo) { + if( !(use instanceof CFGNode cfg) || visit.get(cfg._nid) ) + return; // Been there, done that + assert !( def instanceof ReturnNode && use instanceof CallEndNode ); // All calls unwired now + assert !( def instanceof CallNode && use instanceof FunNode ); // All calls unwired now + visit.set(cfg._nid); + for( Node useuse : cfg._outputs ) + _rpo_cfg(cfg,useuse,visit,rpo); + rpo.add(cfg); + } + + // After early scheduling, every global chain member has Start in slot 0. + // Give each function one private copy of the entire dependency graph. + private static void breakUpGlobalConstants( StartNode start ) { + var globals = new IdentityHashMap(); + var cons = new ArrayList(); + for( Node con : start._outputs ) + if( con!=null && !(con instanceof CFGNode) && + (con.isConst() || con instanceof MachNode mach && mach.outregmap()!=null) ) { + globals.put(con,true); + cons.add(con.keep()); // Preserve original inputs while rewiring users. + } + + var copies = new IdentityHashMap>(); + for( Node con : cons ) + for( Node use : con._outputs.asAry() ) { + if( use==null || globals.containsKey(use) ) continue; + FunNode fun = useFun(use); + if( fun==null ) continue; // Global metadata has no function owner. + assert CodeGen.CODE._mach==null || CodeGen.CODE.owns(fun); + var local = copies.computeIfAbsent(fun,f -> new IdentityHashMap<>()); + Node copy = cloneGlobal(con,fun,globals,local); + for( int i=0; i globals, + IdentityHashMap copies) { + Node copy = copies.get(con); + if( copy!=null ) return copy; + copies.put(con,copy=con.copyEmpty()); + copy.addDef(fun); + for( int i=1; i early.idepth() ) + early = n.in(i).cfg0(); // Latest/deepest input + n.setDef(0,early); + } + } + + // ------------------------------------------------------------------------ + private static void schedLate( CodeGen code ) { + CFGNode[] late = new CFGNode[code.UID()]; + Node[] ns = new Node[code.UID()]; + // Record Load NIDs at all their CFG block choices, then check against + // Store block choices to force a Load above an anti-dependent Store. + int[] anti = new int[code.UID()]; + Ary antideps = new Ary<>(Node.class); + // Breadth-first scheduling + breadth(code._stop,ns,late,anti, antideps); + + // Copy the best placement choice into the control slot + for( int i=0; i antideps) { + // Things on the worklist have some (but perhaps not all) uses done. + WorkList work = new WorkList<>(); + work.push(stop); + Node n; + outer: + while( (n = work.pop()) != null ) { + assert late[n._nid]==null; // No double visit + // These I know the late schedule of, and need to set early for loops + if( n instanceof CFGNode cfg ) late[n._nid] = cfg.blockHead() ? cfg : cfg.cfg(0); + else if( n instanceof PhiNode phi ) late[n._nid] = phi.region(); + else if( n instanceof ProjNode && n.in(0) instanceof CFGNode cfg ) late[n._nid] = cfg; + else { + + // All uses done? + for( Node use : n._outputs ) + if( use!=null && late[use._nid]==null ) + continue outer; // Nope, await all uses done + + // Loads need their memory inputs' uses also done + if( n instanceof MemOpNode ld && ld._isLoad ) + for( Node memuse : ld.antiDeps(antideps) ) + if( late[memuse._nid]==null ) + continue outer; + + // All uses done, schedule + _doSchedLate(n,ns,late,anti,antideps); + } + + // A use just finished; reconsider its inputs and waiting loads, + // even when the shared memory input was already scheduled. + for( Node def : n._inputs ) { + if( def==null ) continue; + if( late[def._nid]==null ) work.push(def); + wakeLoads(def,late,work,new BitSet()); + } + if( n instanceof LoopNode loop ) + for( Node phi : loop._outputs ) + if( phi instanceof PhiNode && late[phi._nid]==null ) + work.push(phi); + } + } + + private static void _doSchedLate(Node n, Node[] ns, CFGNode[] late, int[] anti, Ary antideps) { + // Walk uses, gathering the LCA (Least Common Ancestor) of uses + CFGNode early = n.in(0) instanceof CFGNode cfg ? cfg : n.in(0).cfg0(); + assert early != null; + CFGNode lca = null; + for( Node use : n._outputs ) + if( use != null ) + lca = use_block(n,use, late).domLCA(lca,null); + + // Loads may need anti-dependencies, raising their LCA + if( n instanceof MemOpNode load && load._isLoad ) + lca = find_anti_dep(lca,load,early,late,anti, antideps); + + // Walk up from the LCA to the early, looking for best place. This is + // the lowest execution frequency, approximated by least loop depth and + // deepest control flow. + CFGNode best = lca; + lca = lca.idom(); // Already found best for starting LCA + for( ; lca != early.idom(); lca = lca.idom() ) + if( better(lca,best) ) + best = lca; + assert !(best instanceof IfNode); + ns [n._nid] = n; + late[n._nid] = best; + } + + // Block of use. Normally from late[] schedule, except for Phis, which go + // to the matching Region input. + private static CFGNode use_block(Node n, Node use, CFGNode[] late) { + if( use instanceof ParmNode parm ) + return late[use._nid]; + if( !(use instanceof PhiNode phi) ) + return late[use._nid]; + CFGNode found=null; + for( int i=1; i best.idepth() || + best instanceof IfNode; + } + + // A writer may consume a load's memory through an aggregate. + private static void wakeLoads(Node def, CFGNode[] late, WorkList work, BitSet visit) { + if( visit.get(def._nid) ) return; + visit.set(def._nid); + for( Node out : def._outputs ) + if( out instanceof MemOpNode ld && ld._isLoad && late[ld._nid]==null ) work.push(ld); + if( def instanceof MemMergeNode ) + for( int i=1; i antideps) { + // We could skip final-field loads here. + // Walk LCA->early, flagging Load's block location choices + for( CFGNode cfg=lca; cfg!=early.idom(); cfg = cfg.idom() ) + anti[cfg._nid] = load._nid; + // Walk load->mem uses, looking for Stores causing an anti-dep + for( Node mem : load.antiDeps(antideps) ) { + if( mem instanceof PhiNode phi ) { + // Inputs carrying different memory might overwrite this alias. + // Raise the LCA for those paths; the unchanged-memory paths + // do not clobber the load. No scheduling edge to the Phi. + for( int i=1; i= defblk.idepth(); stblk = stblk.idom() ) { + // Store and Load overlap, need anti-dependence + if( anti[stblk._nid]==load._nid ) { + lca = stblk.domLCA(lca,null); // Raise Loads LCA + if( lca == stblk && st != null && !(st instanceof CallNode) && st._inputs.find(load) == -1 ) // And if something moved, + st.addDef(load); // Add anti-dep as well (calls already end their block) + return lca; // Cap this stores' anti-dep to here + } + } + return lca; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/IFG.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/IFG.java new file mode 100644 index 00000000..bcdb14bf --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/IFG.java @@ -0,0 +1,580 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import java.util.BitSet; +import java.util.IdentityHashMap; + +// Interference Graph +abstract public class IFG { + // Interference Graph Support + + // Map from a Basic Block to Live-Out: {a map from a Live Range to a Def} + private static final IdentityHashMap> BBOUTS = new IdentityHashMap<>(); + static void resetBBLiveOut() { + for( IdentityHashMap bbout : BBOUTS.values() ) + bbout.clear(); + } + static final IdentityHashMap TMP = new IdentityHashMap<>(); + + // Inteference Graph: Array of Bitsets + static final Ary IFG = new Ary<>(BitSet.class); + static void resetIFG() { + for( BitSet bs : IFG ) + if( bs!=null ) bs.clear(); + } + + // Set matching bit + private static void addIFG( LRG lrg0, LRG lrg1 ) { + short x0 = lrg0._lrg, x1 = lrg1._lrg; + // Triangulate + if( x0 < x1 ) _addIFG(x0,x1); + else _addIFG(x1,x0); + } + // Add lrg1 to lrg0's conflict set + private static void _addIFG( short x0, short x1 ) { + BitSet ifg = IFG.atX(x0); + if( ifg==null ) + IFG.setX(x0,ifg = new BitSet()); + ifg.set(x1); + } + + + private static final Ary WORK = new Ary<>(CFGNode.class); + static void pushWork(CFGNode bb) { + if( WORK.find(bb) == -1 ) + WORK.push(bb); + } + + // ------------------------------------------------------------------------ + // Visit all blocks, using the live-out LRGs per-block and doing a backwards + // walk over each block. At the end of the walk, push the live-out sets to + // prior blocks. Due to loops, no single visitation order suffices. This + // problem can be fairly efficiently solved with a proper LIVE calculation + // and bitsets. In the interests of simplicity (and assumption of smaller + // programs) I am skipping LIVE and directly computing it during the IFG + // building. + + // Start by setting the live-out of the exit block (to empty), and putting + // it on a worklist. + + // - Pull from worklist a block who's live-out has changed + // - Walk backwards, adding interferences and computing live. + // - At block head, "push" the new live-out to prior blocks. + // - - If they pick up new live-outs, put them on the worklist. + + public static boolean build( int round, RegAlloc alloc ) { + // Reset all to empty + resetBBLiveOut(); + resetIFG(); + WORK.clear(); + + // Last block has nothing live out + for( CFGNode bb : alloc._code._cfg ) + if( bb.blockHead()) + WORK.push( bb ); + + // Process blocks until no more changes + while( !WORK.isEmpty() ) + do_block(round,alloc,WORK.pop()); + + if( alloc.success() ) + convert2DAdjacency(alloc); + return alloc.success(); + } + + // Walk one block backwards, compute live-in from live-out, and build IFG + static void do_block( int round, RegAlloc alloc, CFGNode bb ) { + assert bb.blockHead(); + IdentityHashMap live_out = BBOUTS.get(bb); + TMP.clear(); + if( live_out != null ) + TMP.putAll(live_out); // Copy bits to temp + + // A backwards walk over instructions in the basic block + for( int inum = bb.nOuts()-1; inum >= 0; inum-- ) { + Node n = bb.out(inum); + if( n.in(0) != bb ) continue; + // In a backwards walk, proj users come before the node itself + if( n instanceof MultiNode ) + for( Node proj : n.outs() ) + if( proj instanceof ProjNode ) + do_node(alloc,proj); + if( n instanceof MachNode ) + do_node(alloc,n); + } + + // The bb head kills register, e.g. a CallEnd and caller-save registers + if( bb instanceof MachNode mach ) + kills(alloc,mach); + + // Push live-sets backwards to priors in CFG. + if( bb instanceof RegionNode ) + for( int i=1; i lrgs = BBOUTS.computeIfAbsent( bb, k -> new IdentityHashMap<>() ); + + for( LRG lrg : TMP.keySet() ) { + Node def = TMP.get(lrg); + // Effective def comes from phi input from prior block + if( def instanceof PhiNode phi && phi.cfg0()==priorbb ) { + assert i!=0; + def = phi.in( i ); + } + // The synthetic exit carries no value on this Phi arm. + if( def._type==Type.TOP ) continue; + Node def_bb = lrgs.get(lrg); + if( def_bb==null ) { + lrgs.put(lrg,def); + pushWork(bb); + } else { + // Alive twice with different definitions; self-conflict + selfConflict(alloc,def,lrg,def_bb); + } + } + } + + + static void convert2DAdjacency( RegAlloc alloc ) { + // Convert the 2-D array of bits (a 1-D array of BitSets) into an + // adjacency matrix. + int maxlrg = alloc._LRGS.length; + for( int i=1; i=0; lrg=ifg.nextSetBit(lrg+1) ) { + LRG lrg1 = alloc._LRGS[lrg]; + lrg0.addNeighbor(lrg1); + lrg1.addNeighbor(lrg0); + } + } + } + } + + + // ------------------------------------------------------------------------ + // Color the inference graph. + + // Coloring works by removing "trivial" live ranges from the IFG - those + // live ranges with fewer neighbors than available colors (registers). + // These are trivial because even if every neighbor takes a unique color, + // there's at least one more available to color this live range. + + // If we hit a clique of non-trivial live ranges, we pick one to be "at + // risk" of not-coloring. Good choices include live ranges with a large + // area and few hot uses. + + // Then we reverse and put-back live ranges - picking a color as we go. + // If there's no spare color we'll have to spill this at-risk live range. + + public static boolean color(int round, RegAlloc alloc) { + int maxlrg = alloc._LRGS.length, nlrgs=0; + for( int i=1; i swork ) + swork = sptr; + + // Walk all neighbors and remove + if( lrg._adj != null ) { + for( LRG nlrg : lrg._adj ) { + // Remove and compress out neighbor + if( nlrg.removeCompress(lrg) ) { + // Neighbor is just exactly going trivial as 'lrg' is removed from IFG + // Find "j" position in the color_stack + int jx = swork; while( color_stack[jx] != nlrg ) jx++; + // Add trivial neighbor to trivial list. Pull lrg j out of + // unknown set, since its now in the trivial set + swap(color_stack, swork++, jx); + } + } + } + } + + // Reverse simplify (unstack the color stack), and set colors (registers) for live ranges + while( sptr > 0 ) { + LRG lrg = color_stack[--sptr]; + if( lrg==null ) continue; + RegMaskRW rmask = lrg._mask.copy(); + // Walk neighbors and remove adjacent colors + if( lrg._adj!=null ) { + for( LRG nlrg : lrg._adj ) { + nlrg.reinsert(lrg); + int reg = nlrg._reg; + if( reg!= -1 ) // Failed neighbors do not count + rmask.clr(reg); // Remove neighbor from my choices + } + } + // At-risk live-range did not color? + if( rmask.isEmpty() ) { + alloc.fail(lrg); + lrg._reg = -1; + } else { + // Pick first available register + short reg = rmask.firstReg(); + // Pick a "good" color from the choices. Typically, biased-coloring + // removes some over-spilling. + if( rmask.size() > 1 ) reg = biasColor(alloc,lrg,reg,rmask); + lrg._reg = reg; // Assign the register + } + } + + return alloc.success(); + } + + // Pick LRG from color stack + private static void pickColor(LRG[] color_stack, int sptr, int swork, RegAlloc alloc) { + // Out of trivial colorable, pick an at-risk to pull + if( sptr==swork ) + swap(color_stack,sptr,pickRisky(color_stack,sptr,alloc)); + // When coloring, we'd like to give more choices; so when coloring we'd + // like to see the single-def first (since no choices anyway), then + // non-split related (so more live ranges get colored), then + // split-related last, so they have more colors to bias towards. + + // Working in reverse, pick first split-related with many regs, then + // those with some regs, then single-def. + int bidx=sptr; + LRG best=color_stack[bidx]; + for( int idx = sptr+1; idx < swork; idx++ ) + if( betterLRG(best,color_stack[idx]) ) + best = color_stack[bidx=idx]; + if( bidx != sptr ) + swap(color_stack,sptr,bidx); // Pick best at sptr + } + + // Returns true if 'lrg' is better than 'best' + private static boolean betterLRG( LRG best, LRG lrg ) { + // If single-def varies, keep the not-single-def + if( best.size1() != lrg.size1() ) + return best.size1(); + // If hasSplit varies, keep the hasSplit + if( best.hasSplit() != lrg.hasSplit() ) + return lrg.hasSplit(); + // If both are single register (so both sizes are 1), + // return a multi-use over a multi-def + if( best.size1() ) + return !best._multiUse && lrg._multiUse; + // Keep large register count + return best.size() < lrg.size(); + } + + // Pick a live range that hasn't already spilled, or has a single-def- + // single-use that are not adjacent. + private static int pickRisky( LRG[] color_stack, int sptr, RegAlloc alloc ) { + int best=sptr; + int bestScore = pickRiskyScore(color_stack[best],alloc); + for( int i=sptr+1; i bestScore ) + { best = i; bestScore = iScore; } + } + return best; + } + + // Pick a live range to pull, that might not color. + // + // Picking a live range with a very large span, with defs and uses outside + // loops means spilling a relative cheap live range and getting that + // register over a large area. + // + // Picking a live range that is very close to coloring might allow it to + // color despite being risky. + private static int pickRiskyScore( LRG lrg, RegAlloc alloc ) { + // Callee-save live ranges cover the whole function and are cheap to + // split. Rank scheduled area recovered against the loop-scaled cost + // of the def- and use-side moves. + if( lrg._machDef instanceof CalleeSaveNode ) + return areaScore(functionArea(lrg._fun,alloc), + splitCost((Node)lrg._machDef,(Node)lrg._machUse)); + if( lrg._splitDef != null && lrg._splitDef. in(1) instanceof CalleeSaveNode && + lrg._splitUse != null && lrg._splitUse.out(0) instanceof ReturnNode ) + return areaScore(functionArea(lrg._fun,alloc), + splitCost(lrg._splitDef,lrg._splitUse)); + + // Pick single-def clonables that are not right next to their single-use. + // Rank by the area recovered by cloning closer to the use. + if( !lrg._multiDef && lrg._machDef!=null && lrg._machUse!=null && lrg._machDef.isClone() ) { + Node def = ((Node)lrg._machDef); + Node use = ((Node)lrg._machUse); + CFGNode cfg = def.cfg0(); + int area = cfg != use.cfg0() + ? functionArea(lrg._fun,alloc) + : cfg._outputs.find(use)-cfg._outputs.find(def)-1; + // A single-use function address can move next to its use without + // adding an instruction. Prefer that to saving a callee-save + // register just to hold the address. Stay in the same block and + // require a gap so the clone makes progress. + if( def instanceof FunPtrNode && !lrg._multiUse && + cfg == use.cfg0() && area > 0 ) + return 1000000; + if( area > 0 ) + return areaScore(area,loopCost(use)); + } + + // Default for ranges without a specific area/cost estimate. + return 1000 + (lrg._multiUse ? -100 : 0); + } + + private static int functionArea(FunNode fun, RegAlloc alloc) { + int area=0; + for( CFGNode bb : alloc._code._cfg ) + if( bb.fun()==fun ) + area += bb.nOuts(); + return area; + } + + private static int areaScore(int area, int cost) { + return 1000 + (area<<10)/cost; + } + + private static int splitCost(Node def, Node use) { + return loopCost(def) + (use==null ? 0 : loopCost(use)); + } + + private static int loopCost(Node n) { + return 1 << (n.cfg0().loopDepth()*3); + } + + private static short biasColor( RegAlloc alloc, LRG lrg, short reg, RegMask mask ) { + if( mask.size1() ) return reg; + // Check chain of splits up the def-chain. Take first allocated + // register, and if it's available in the mask, take it. + Node def = lrg._splitDef, use = lrg._splitUse; + int tidx=0, cnt=0; + + while( def != null || use != null ) { + if( cnt++ > 10 ) break; + + if( def != null ) { + short bias = biasColor( alloc, def, mask ); + if( bias >= 0 ) return bias; // Good bias + if( bias == -2 ) def = null; // Kill this side, no more searching + else if( (tidx=biasable(def)) == 0 ) def = null; + } + + if( use != null ) { + short bias = biasColor( alloc, use, mask ); + if( bias >= 0 ) return bias; // Good bias + if( bias == -2 ) use = null; // Kill this side, no more searching + else if( biasable(use)==0 ) use = null; + } + + if( def != null ) { + short bias = biasColorNeighbors( alloc, def, mask ); + if( bias >= 0 ) return bias; + // Advance def side + def = def.in(tidx); + if( alloc.lrg(def)==null ) def=null; + } + + if( use != null ) { + short bias = biasColorNeighbors( alloc, use, mask ); + if( bias >= 0 ) return bias; + use = use.out(0); + if( biasable(use)==0 ) use=null; + } + + } + return mask.firstReg(); + } + + private static int biasable(Node split) { + if( split instanceof SplitNode ) return 1; // Yes biasable, advance is slot 1 + if( split instanceof PhiNode phi ) return phi.region() instanceof LoopNode ? 2 : 1; // Prefer the backedge in loops + if( !(split instanceof MachNode mach) ) return 0; // Not biasable + return mach.twoAddress(); // Only biasable if 2-addr + } + + // 3-way return: + // - good bias reg, take it & exit + // - this path is cutoff; do not search here anymore + // - advance this side + private static short biasColor( RegAlloc alloc, Node split, RegMask mask ) { + short bias = alloc.lrg(split)._reg; + if( bias != -1 ) { + if( mask.test(bias) ) return bias; // Good bias + else return -2; // Kill this side + } else return -1; // Advance this side + } + + // Check if we can match "split" color, or else trim "mask" to colors + // "split" might get. + private static short biasColorNeighbors( RegAlloc alloc, Node split, RegMask mask ) { + LRG slrg = alloc.lrg(split); + if( slrg._adj == null ) return -1; // No trimming + + // Can I limit my own choices to valid neighbor choices? + for( LRG alrg : slrg._adj ) { + short reg = alrg._reg; + if( reg == -1 && alrg._mask.size1() ) + reg = alrg._mask.firstReg(); + if( reg != -1 ) { + mask.clr(reg); + if( mask.size1() ) + return mask.firstReg(); + } + } + return -1; // No obvious color choice, but mask got trimmed + } + + private static void swap( LRG[] ary, int x, int y ) { + LRG tmp = ary[x]; ary[x] = ary[y]; ary[y] = tmp; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java new file mode 100644 index 00000000..db35d524 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java @@ -0,0 +1,126 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; +import java.util.IdentityHashMap; + +// Replace ideal nodes with machine nodes while preserving CFG/loop metadata. +// Selection also owns target ABI masks and rebuilding the copied graph's uses. +class InstSelect { + private final CodeGen _code; + private InstSelect(CodeGen code) { _code=code; } + + static CodeGen select(CodeGen code, String cpu, String callingConv, String base) { + // Ideal evaluation schedules the ideal graph without a CPU port. + if( cpu==null ) return code; + return code.phase(CodeGen.Phase.Select, code._phase==CodeGen.Phase.LoopTree, + () -> new InstSelect(code).run(cpu,callingConv,base)); + } + + private void run(String cpu, String callingConv, String base) { + _code._callingConv = callingConv; + + // Look for CPU in fixed named place: + // com.seaofnodes.simple.node.cpus."cpu"."cpu.class" + String clzFile = base+"."+cpu+"."+cpu; + try { _code._mach = Class.forName(clzFile).asSubclass(Machine.class).getDeclaredConstructor(CodeGen.class).newInstance(_code); } + catch( Exception e ) { throw new RuntimeException(e); } + + _code._cCallingConv = _code._mach.cCallingConv(callingConv); + + // Build global copies of common register masks. + long callerSave = _code._mach.callerSave(); + long cCallerSave= _code._mach.callerSave(_code._cCallingConv); + long neverSave = _code._mach. neverSave(); + int maxReg = Math.min(64,_code._mach.regs().length); + assert maxReg>=64 || (-1L << maxReg & callerSave)==0; // No stack slots in callerSave + assert maxReg>=64 || (-1L << maxReg & cCallerSave)==0; // No stack slots in cCallerSave + _code._callerSave = new RegMask(callerSave); + _code._cCallerSave= new RegMask(cCallerSave); + + // Build a Return RegMask array. All returns have the following inputs: + // 0 - ctrl + // 1 - memory + // 2 - varies with returning GPR,FPR + // 3 - RPC + // 4+- Caller save registers + _code._retMasks = new RegMask[(maxReg-_code._callerSave.size()-Long.bitCount( neverSave ))+4]; + for( int reg=0, i=4; reg(); + _instSelect( _code._stop, map ); + _code._stop = (StopNode)map.get(_code._stop ); + StartNode start = (StartNode)map.get(_code._start); + _code._start = start==null ? new StartNode(_code._start) : start; + _instOuts(_code._stop,_code.visit()); + _code._visit.clear(); + + // Replace the CompUnit stop (and list of functions) + // with hardware-specific ones + for( CompUnit cu : _code._compunits.values() ) { + cu._start= (StartCUNode)map.get(cu._start); + cu._stop = (StopCUNode )map.get(cu._stop ); + } + } + + private Node _instSelect( Node n, IdentityHashMap map ) { + if( n==null ) return null; + Node x = map.get(n); + if( x !=null ) return x; // Been there, done that + + // If n is a MachNode already, then its part of a multi-node expansion. + // It does not need instruction selection (already selected!) + // but it does need its inputs walked. + if( n instanceof MachNode ) { + for( int i=0; i < n.nIns(); i++ ) + n._inputs.set(i, _instSelect(n.in(i),map) ); + return n; + } + + // Produce a machine node from n; map it to flag as done so stops cycles. + if( n instanceof MemOpNode mem && mem.off() instanceof ExternOffsetNode off ) { + // External fields replace base+offset with symbol+0. Keep the ideal + // declaring type on the address for machine load/store width selection. + TypeMemPtr ptr = (TypeMemPtr)mem.ptr()._type; + n._inputs.set(2,new ExternNode(TypeMemPtr.make((byte)2,ptr._obj,true),off._extern)); + n._inputs.set(3,ConstantNode.raw(TypeInteger.ZERO)); + } + map.put(n, x=_code._mach.instSelect(n) ); + // Carry loop-tree and pre-order info across the ideal->mach transition + if( n instanceof CFGNode ncfg && x instanceof CFGNode xcfg ) { + xcfg._ltree = ncfg._ltree; + xcfg._pre = ncfg._pre; + } + // Walk machine op and replace inputs with mapped inputs + for( int i=0; i < x.nIns(); i++ ) + x._inputs.set(i, _instSelect(x.in(i),map) ); + // Post selection action + if( x instanceof MachNode mach ) { + if( n instanceof ReturnNode ret ) + ((ReturnNode)mach)._fun = (FunNode)map.get(ret._fun); + mach.postSelect(_code); // Post selection action + } + + return x; + } + + private void _instOuts( Node n, BitSet visit ) { + if( visit.get(n._nid) ) return; + visit.set(n._nid); + for( Node in : n._inputs ) + if( in!=null ) { + in._outputs.push(n); + // CallNode special: outputs are partially ordered; CallEnd in slot 0 + if( in instanceof CallNode call && n instanceof CallEndNode cend && call.out(0) != cend ) + call._outputs.swap(0,call.nOuts()-1); + _instOuts(in,visit); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Iter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Iter.java new file mode 100644 index 00000000..1ffd9737 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Iter.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +// Establish a pessimistic fixed point after parsing. Later Iter runs +// reuse the worklist and types produced by the preceding phase. +abstract class Iter { + static void run(CodeGen code) { + // Close parser-in-progress states. Seed a fresh pessimistic solve; + // exposure sets start at FULL and can only shrink during Iter. + code._start.walk(n -> { + if( !(n instanceof ConstantNode) ) + n._type = iterEscapeReset(n._type); + code.add(n); + return null; + }); + code._iter.iterate(code); + // Expensive assert. + assert !CodeGen.expensiveAssert() || Opto.fixedPointCheck(code); + } + + private static Type iterEscapeReset(Type t) { + if( t instanceof TypeMem mem ) + return mem._one ? mem : mem.makeFrom(XInt.FULL,XInt.FULL); + if( t instanceof TypeTuple tt ) { + TypeTuple rez = tt; + for( int i=0; i + * A live range is a set of nodes and edges which must get the same register. + * Live ranges form an interconnected web with almost no limits on their + * shape. Live ranges also gather the set of register constraints from all + * their parts. + *

+ * Most of the fields in this class are for spill heuristics, but there are a + * few key ones that define what a LRG is. LRGs have a unique dense integer + * `_lrg` number which names the LRG. New `_lrg` numbers come from the + * `RegAlloc._lrg_num` counter. LRGs can be unioned together - + * (this is the Union-Find algorithm) + * - and when this happens the lower numbered `_lrg` wins. Unioning only + * happens during `BuildLRG` and happens because either a `Phi` is forcing all + * its inputs and outputs into the same register, or because of a 2-address + * instruction. LRGs have matching `union` and `find` calls, and a set + * `_leader` field. + */ +public class LRG { + + // Dense live range numbers + final short _lrg; + + // U-F leader; null if leader + LRG _leader; + + // Choosen register + short _reg; + + // Count of single-register defs and uses + short _1regDefCnt, _1regUseCnt; + // Count of all defs, uses. Mostly interested 1 vs many + boolean _multiDef, _multiUse; + + // A sample MachNode def in the live range + public MachNode _machDef, _machUse; + short _uidx; // _machUse input + + // Some splits used in biased coloring + MachConcreteNode _splitDef, _splitUse; + + // All the self-conflicting defs for this live range + IdentityHashMap _selfConflicts; + + // AND of all masks involved; null if none have been applied yet + RegMask _mask; + + // Adjacent Live Range neighbors. Only valid during coloring + Ary _adj; + + // Function fully containing the live range; used to find the stack frame + // layout, for spill op offsets. + final public FunNode _fun; + + public int nadj() { return _adj==null ? 0 : _adj._len; } + + public RegMask useMask() { return _machUse==null ? null : _machUse.regmap(_uidx); } + + void addNeighbor(LRG lrg) { + if( _adj==null ) _adj = new Ary<>(LRG.class); + _adj.push(lrg); + } + + // Remove and compress neighbor list; store ex-neighbor past + // end for unwinding Simplify. True if exactly going low-degree + boolean removeCompress( LRG lrg ) { + _adj.swap(_adj.find(lrg),_adj._len-1); + return _adj._len-- == _mask.size(); + } + + void reinsert( LRG lrg ) { + assert _adj._es[_adj._len]==lrg; + _adj._len++; + } + + // More registers than neighbors + boolean lowDegree() { return nadj() < _mask.size(); } + + LRG( short lrg, FunNode fun ) { _lrg = lrg; _reg = -1; _fun = fun; } + + boolean leader() { return _leader == null; } + + // Fast-path Find from the Union-Find algorithm + LRG find() { + if( _leader==null ) return this; // I am the leader + if( _leader._leader==null ) // I point to the leader + return _leader; + return _rollup(); + } + // Slow-path rollup of U-F + private LRG _rollup() { + LRG ldr = _leader._leader; + // Roll-up + while( ldr._leader!=null ) ldr = ldr._leader; + LRG l2 = this; + while( l2 != ldr ) { + LRG l3 = l2._leader; + l2._leader = ldr; + l2 = l3; + } + return ldr; + } + + // Union `this` and `lrg`, keeping the lower numbered _lrg. + // Includes a number of fast-path cutouts. + LRG union( LRG lrg ) { + assert leader(); + if( lrg==null ) return this; + lrg = lrg.find(); + if( lrg==this ) return this; + return _lrg < lrg._lrg ? _union(lrg) : lrg._union(this); + } + // Union `this` and `lrg`, folding together all stats. + private LRG _union( LRG lrg ) { + assert _fun == lrg._fun; + // Set U-F leader + lrg._leader = this; + // Fold together stats + if( _machDef==null ) { + _machDef = lrg._machDef; + } else if( lrg._machDef!=null ) { + if( _machDef != lrg._machDef ) _multiDef=true; + if( _1regDefCnt==0 ) + _machDef = lrg._machDef; + else if( _machDef==lrg._machDef ) + _1regDefCnt--; + } + _1regDefCnt += lrg._1regDefCnt; + _multiDef |= lrg._multiDef; + + if( _machUse==null ) { + _machUse = lrg._machUse; + _uidx = lrg._uidx; + } else if( lrg._machUse!=null ) { + if( _machUse != lrg._machUse ) _multiUse=true; + if( _1regUseCnt==0 ) { + _machUse = lrg._machUse; + _uidx = lrg._uidx; + } + else if( _machUse==lrg._machUse ) + _1regUseCnt--; + } + _1regUseCnt += lrg._1regUseCnt; + _multiUse |= lrg._multiUse; + + // Fold deepest Split + _splitDef = deepSplit(_splitDef,lrg._splitDef); + _splitUse = deepSplit(_splitUse,lrg._splitUse); + + // Fold together masks + RegMask mask = _mask.and(lrg._mask); + if( mask==null ) + mask = _mask.copy().and(lrg._mask); + _mask = mask; + return this; + } + + private static MachConcreteNode deepSplit( MachConcreteNode s0, MachConcreteNode s1 ) { + return s0==null || (s1!=null && s0.cfg0().loopDepth() < s1.cfg0().loopDepth()) ? s1 : s0; + } + + // Record any Mach def for spilling heuristics + LRG machDef( MachNode def, boolean size1 ) { + if( _machDef!=null && _machDef!=def ) + _multiDef = true; + if( _machDef==null || size1 ) + _machDef = def; + if( size1 ) + _1regDefCnt++; + if( def instanceof SplitNode split ) + _splitDef = deepSplit(_splitDef,split); + return this; + } + + // Record any Mach use for spilling heuristics + LRG machUse( MachNode use, short uidx, boolean size1 ) { + if( _machUse!=null && _machUse!=use ) + _multiUse = true; + if( _machUse==null || size1 ) + { _machUse = use; _uidx = uidx; } + if( size1 ) + _1regUseCnt++; + if( use instanceof SplitNode split ) + _splitUse = deepSplit(_splitUse,split); + return this; + } + + boolean hasSplit() { return _splitDef != null || _splitUse != null; } + short size() { return _mask.size(); } + boolean size1() { return _mask.size1(); } + + + void selfConflict( Node def ) { + if( _selfConflicts == null ) _selfConflicts = new IdentityHashMap<>(); + _selfConflicts.put(def,""); + } + + + // Record intersection of all register masks. + // True if still has registers + boolean and( RegMask mask ) { + RegMask mask2 = mask.and(_mask); + if( mask2==null ) + mask2 = _mask.copy().and(mask); + _mask = mask2; + return !_mask.isEmpty(); + } + // Remove this singular register + // True if still has registers + boolean clr( int reg ) { + if( _mask.clr(reg) ) return true; + _mask = _mask.copy(); // Need a mutable copy + return _mask.clr(reg); + } + // Subtract mask (AND complement) + // True if still has registers + boolean sub( RegMask mask ) { + RegMask mask2 = _mask.sub(mask); + if( mask2==null ) + mask2 = _mask.copy().sub(mask); + _mask = mask2; + return !_mask.isEmpty(); + } + + @Override public String toString() { return toString(new SB()).toString(); } + + public SB toString( SB sb ) { + sb.p("V").p(_lrg); + if( _mask!=null ) _mask.toString(sb); + return sb; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java new file mode 100644 index 00000000..252b5bf0 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// In-memory linking +public class LinkMem { + final CodeGen _code; + LinkMem( CodeGen code ) { _code = code; } + + public CodeGen link(Encoding enc) { + // Patch external calls internally + enc.patchGlobalRelocations(); + + // Place constant pool after code, static data after constants + int code = 0; // Code start + int cpool = (enc._bits .size() + code + 15) & -16; // pad to 16 + int sdata = (enc._cpool.size() + cpool + 15) & -16; + + + // Patch local ops, e.g. loading float constants from the constant pool + for( Node op : enc._bigCons.keySet() ) { + Encoding.Relo relo = enc._bigCons.get(op); + int target = enc._data.address(enc._data.object(relo._t),cpool,sdata); + ((RIPRelSize)relo._op).patch(enc, relo._opStart, enc.opLen(relo._op), target - relo._opStart); + } + + enc._data.link(cpool,sdata); + return _code; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java new file mode 100644 index 00000000..b6b3a22c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java @@ -0,0 +1,261 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.IterPeeps.WorkList; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import java.util.*; + +public abstract class ListScheduler { + + // eXtra per-node stuff for scheduling. + // Nodes are all block-local, and projections fold into their parent. + private static final IdentityHashMap XS = new IdentityHashMap<>(); + private static class XSched { + + Node _n; // Node this extra is for + int _bcnt; // Not-ready not-scheduled inputs + int _rcnt; // Ready not-scheduled inputs + boolean _ruse; // Node IS a "remote use", uses a other-block value + boolean _rdef; // Node IS a "remote def" of a value used in other block + boolean _single; // Defines a single register, likely to conflict + + static final Ary FREE = new Ary<>(XSched.class); + static void alloc(CFGNode bb, Node n) { + XSched x = FREE.pop(); + XS.put(n, (x==null ? new XSched() : x).init(bb,n)); + } + void free() { _n = null; FREE.push(this); } + + static XSched get(Node n) { return n==null ? null : XS.get(n instanceof ProjNode && !(n.in(0) instanceof CFGNode) ? n.in(0) : n); } + + private XSched init(CFGNode bb, Node n) { + _n = n; + _bcnt = 0; + _ruse = _rdef = _single = false; + + // Count block-locals and remote inputs + if( !(n instanceof PhiNode) ) + for( Node def : n._inputs ) + if( def != null && def != bb ) { + if( def.cfg0()==bb ) _bcnt++; // Raise block-local input count + else if( !def.isMem() ) _ruse = true; // Remote register user + } + // Count remote outputs and allowed registers + if( n instanceof MultiNode ) + for( Node use : n._outputs ) + computeSingleRDef(bb,use); + else + computeSingleRDef(bb,n); + + if( n instanceof NewNode || n instanceof CallNode ) + bb.fun()._hasCalls = true; + return this; + } + + private void computeSingleRDef(CFGNode bb, Node n) { + // See if this is 2-input, and that input is single-def + if( n instanceof MachNode mach && mach.twoAddress() != 0 ) { + XSched xs = XS.get(n.in(mach.twoAddress())); + if( xs != null ) + _single = xs._single; + } + // Visit all outputs + RegMaskRW rmask = !_single && n instanceof MachNode mach && mach.outregmap() != null ? mach.outregmap().copy() : null; + for( Node use : n._outputs ) { + // Remote use, so this is a remote def + if( use!=null && use.cfg0()!=bb ) _rdef = true; + // And mask between n's def mask and all n's users masks' + if( use instanceof MachNode mach && rmask != null ) + for( int i=1; i 0 || _rcnt > 0 ) return false; + if( _n instanceof MultiNode ) + for( Node use : _n._outputs ) + if( use instanceof ProjNode ) readyUp(use); + // anti-dep from a multireg directly + else _readyUp(use); + else + readyUp(_n); + return true; + } + private static void readyUp(Node n) { + for( Node use : n._outputs ) + _readyUp(use); + } + private static void _readyUp(Node use) { + XSched xs = get(use); + if( xs!=null && !(use instanceof PhiNode) ) + { assert xs._bcnt>0; xs._bcnt--; xs._rcnt++; } + } + + boolean decIsReady() { + assert _rcnt > 0; + _rcnt--; + return isReady(); + } + } + + + // List schedule every block + public static void sched( CodeGen code ) { + XS.clear(); + for( CFGNode bb : code._cfg ) + if( bb.blockHead() ) + local(bb,false); + } + + // Classic list scheduler + private static void local( CFGNode bb, boolean trace ) { + assert XS.isEmpty(); + int len = bb.nOuts(); + + // Count block-locals, remote inputs and outputs + for( int i=0; i 0; + + // Classic list scheduler. Behind sched is scheduled. Between sched + // and ready have zero counts and are ready to schedule. Ahead of + // ready is undefined. + // + // As nodes are scheduled, the nodes they use decrement their bcnts[]. Once + // that gets to 0, all the nodes using it are scheduled, so it's ready. + int sched = 0; + while( sched < len ) { + assert sched < ready; + int pick = best(bb._outputs,sched,ready,trace); // Pick best + Node best = bb._outputs.swap(pick,sched++); // Swap best to head of ready and move into the scheduled set + // Lower ready count of users, and since projections are ready as + // soon as their multi is ready, lower those now too. + for( Node use : best._outputs ) + if( use instanceof ProjNode ) + for( Node useuse : use._outputs ) + ready = ready(bb,useuse,ready); + else + ready = ready(bb,use,ready); + } + + // Reset for next block + for( XSched x : XS.values() ) + x.free(); + XS.clear(); + } + + private static int ready(CFGNode bb, Node use, int ready) { + if( use!=null && use.in(0)==bb && !(use instanceof PhiNode) && XSched.get(use).decIsReady() ) + bb._outputs.set(ready++, use); // Became ready, move into ready set + //bb._outputs.swap(ready++,use); // Became ready, move into ready set + return ready; + } + + + // Pick best between sched and ready. + private static int best( Ary blk, int sched, int ready, boolean trace ) { + int pick = sched; + Node p = blk.at(pick); + int score = score(p); + if( trace ) { System.out.printf("%4d N%d ",score,p._nid); System.out.println(p); } + for( int i=sched+1; i score ) + { score = nscore; pick = i; p = n; } + } + if( trace ) { System.out.printf("Pick: N%d %s\n\n",p._nid,p); } + return pick; + } + + // Highest score wins. Max at 1000, min at 10, except specials. + private static final int[] CNT = new int[3]; + static int score( Node n ) { + if( n instanceof ProjNode ) return 1001; // Pinned at block entry + if( n instanceof CProjNode ) return 1001; // Pinned at block entry + if( n instanceof PhiNode ) return 1000; + if( n instanceof CFGNode ) return 1; // Pinned at block exit + + int score = 500; + // Ideal nodes ignore register pressure scheduling + if( !(n instanceof MachNode) ) + return score; + + // Register pressure local scheduling: avoid overlapping specialized + // registers usages. + + // Subtract 10 (delay) if this op forces a live range to exist that + // cannot be resolved immediately; i.e., live count goes up. Scale to + // 20 if multi-def. + + // Subtract 100 if this op forces a single-def live range to exist + // which might conflict on the same register with other live ranges. + // Defines a single register based on def & uses, and the output is not + // ready. Scale to 200 for multi-def. + CNT[1]=CNT[2]=0; + XSched xn = XSched.get(n); + // If defining a remote value, just generically stall alot. Value is + // used in a later block, can we delay until the end of this block? + if( xn._rdef ) score = 200; // If defining a remote value, just generically stall alot + boolean flags = false; + if( n instanceof MultiNode ) { + for( Node use : n._outputs ) + flags |= singleUseNotReady( use, xn._single ); + } else + flags |= singleUseNotReady( n, xn._single ); + score += -10 * Math.min( CNT[1], 2 ); + score += -100 * Math.min( CNT[2], 2 ); + if( flags ) return 10; + + // Add 10 if this op ends a single-def live range, add 20 for 2 or more. + // Scale to 100,200 if the single-def is also single-register. + CNT[1]=CNT[2]=0; + for( int i=1; i1 && n instanceof MachNode mach && mach.isClone() ) + return false; + for( Node use : n.outs() ) { + XSched xu = XSched.get(use); + if( xu != null && xu._n instanceof CFGNode ) return true; + if( xu != null && xu._bcnt > 0 ) + CNT[single ? 2 : 1]++; // Since bcnt>0 or CFG, stall until user is more ready + } + return false; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java new file mode 100644 index 00000000..462248dd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.codegen; + +import java.util.HashMap; +import java.util.Arrays; + +// Dense IDs for this invocation. A class slot can be reserved before its file +// is parsed; every later reference to that slot gets the same ID. +public class LocalIds { + public static final int RESERVED=2; + private int _next=RESERVED; + private final com.seaofnodes.simple.util.Ary _owners=new com.seaofnodes.simple.util.Ary<>(String.class); + private final HashMap _slots=new HashMap<>(); + + public int next(String owner) { return next(owner,-1); } + public int next(String owner, int slot) { + int[] ids=_slots.get(owner); + if( ids==null ) ids=new int[]{RESERVED,1}; + if( slot==-1 ) slot=ids[0]++; + else if( slot>=ids[0] ) ids[0]=slot+1; + if( slot>=ids.length ) ids=Arrays.copyOf(ids,Math.max(slot+1,ids.length*2)); + if( ids[slot]==0 ) { ids[slot]=_next++; _owners.setX(ids[slot],owner); } + _slots.put(owner,ids); + return ids[slot]; + } + public String owner(int id) { return _owners.atX(id); } + public String symbol(int id) { return owner(id)+".$fun"+id; } + public int nextInline() { return _next++; } + public int next(int old) { + int id=_next++; + _owners.setX(id,owner(old)); + return id; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Machine.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Machine.java new file mode 100644 index 00000000..3eee8fc3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Machine.java @@ -0,0 +1,127 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.TypeInteger; + +abstract public class Machine { + // Human readable machine name. Something like "x86-64" or "arm" or "risc5" + public abstract String name(); + + // Default opcode size for printing; 4 bytes for most 32-bit risc chips + public int defaultOpSize() { return 4; } + + // Human-readable name for a register number, e.g. "RAX" or "R0" + public abstract String[] regs(); + + // Create a split op; any register to any register, including stack slots. + // The FunNode holds the stack layout after RegAlloc, which in turn + // dictates the stack slot (the frame offset) for stack splits. + public abstract SplitNode split( LRG lrg, String kind, byte round ); + + // List of caller-save registers, as a 64bit mask + public abstract long callerSave(); + public long callerSave(String conv) { return callerSave(); } + + // List of never-save registers, e.g. RSP or a ZERO register if you have one + public abstract long neverSave(); + + // Call Argument Mask. Passed in the function signature and argument + // number (2-based; 0 is for control and 1 for memory). Also passed in a 0 + // for the function itself, or for *outgoing* calls, the maximum stack slot + // given to the incoming function arguments (stack slots reserved for + // incoming arguments). + public abstract RegMask callArgMask(TypeFunPtr tfp, int arg, int maxArgSlot); + public RegMask callArgMask(TypeFunPtr tfp, int arg, int maxArgSlot, String conv) { return callArgMask(tfp,arg,maxArgSlot); } + + // Return register mask, based on signature (GPR vs FPR) + public abstract RegMask retMask(TypeFunPtr tfp); + + // Return PC register + public abstract int rpc(); + + // Return a MachNode unconditional branch + public abstract CFGNode jump(); + + // Instruction select from ideal nodes + public abstract Node instSelect( Node n ); + + /** Stack Slot Numbering + +RA gives all registers a number, starting at 0. Stack slots continue this +numbering going up from the last register number. Common register numbers are +0-15 for GPRs, 16-31 for FPRs, 32 for flags, and stack slots starting at +register#33 going up. These numbers are machine-specific, YMMV, etc; e.g. RPC +is only in stack slot 0 on X86; other cpus start with the rpc in a register +which may spill into any generic spill slot. + +For ease of reading and printing, stack slots start again at slot#0 - but +during RA they are actually biased by CPU.MAX_REG. + +Stack Layout during Reg Alloc: + + Slot Value Description + ---- ------- ----------- +MAXREG+ R+1 argR maxArgSlot(calleR tfp), can be 0 + ... +MAXREG+ 1 arg0 + ---- -- Caller ------ +MAXREG+ 0 RPC +MAXREG+ R+E+N PAD optional alignment + +MAXREG+ + Spills Caller Frame Space; N spills +MAXREG+ R+E+1 Spills + +MAXREG+ R+E argE maxArgSlot(calleE tfp), can be 0 + ... +MAXREG+ R+2 arg0 + ---- -- Callee ------- + ... + +Slots defined by maxArgSlot do not have to contain the actual passed argument; +they can be "shadow slots". If they exist, they are property of the frame / +FunNode where the TFP comes from. + +For a given calleR, there are generally many calleE's and they all share the +low-range stack slots. + +Example:, Win64, main(int arg) { calloc(1,size); } + + Post-Warp: SP+56 shadow-main-arg +Pre-alloc Post-Alloc SP+48 RPC +--- ------- ---------- ------------- +40 spill#3 SP+40 SP+40 spill#3 +39 spill#2 SP+32 "" +38 spill#1 SP+24 "" +37 spill#0 SP+16 "" spill#0 +36 shadow-calloc#size SP+ 8 SP+ 8 shadow-calloc#size +35 shadow-calloc#1 SP+ 0 SP+ 0 shadow-calloc#1 +--- --- FRAME BASE ---------- ---- FRAME BASE ------- +34 shadow-main-arg SP+56 +33 RPC SP+48 + + + */ + + + // Maximum stack slot (or 0) for the args in this TFP. This will include + // shadow slots if defined in the ABI, even if all arguments are passed in + // registers. + public abstract short maxArgSlot(TypeFunPtr tfp); + public short maxArgSlot(TypeFunPtr tfp, String conv) { return maxArgSlot(tfp); } + + // Calling convention used for external C symbols. + public String cCallingConv(String callingConv) { return callingConv; } + + // Only the declared C result width describes the physical return register. + // Simple functions already return canonical full-width integer values. + protected static TypeInteger cReturn(ProjNode prj) { + if( prj._idx==2 && prj.in(0) instanceof CallEndNode cend && + cend.call().fptr()._type instanceof TypeFunPtr tfp && tfp.isConstant() && + CodeGen.CODE.externFunc(tfp.fidx())!=null && + tfp.ret() instanceof TypeInteger ret && ret.log_size()<3 ) + return ret; + return null; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Opto.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Opto.java new file mode 100644 index 00000000..4db277df --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Opto.java @@ -0,0 +1,317 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +import java.util.BitSet; + +// Optimistic Algorithm +abstract public class Opto { + + // Optimistic analysis - extended SCCP + + // Start all Types at TOP, and propagate changes until stable. Unlike a + // normal Sparse Conditional Constant Propagation, this version is + // interprocedural, computing effectively CFA0 in linear time using the + // normal SCCP algorithm. + + // This algo also builds a Call Graph; as Calls become reachable (CONTROL + // not TOP nor XCONTROL), they "link" against the reaching function + // pointers - adding edges from the Call to the Fun, and from the Return to + // the CallEnd. This allows the normal algorithm to use the normal edge + // propagation rules as function argument types stabilize. In the end, we + // get a fairly precise set of Call Graph edges between CallNodes and + // FunNodes. + public static void opto(CodeGen code) { + // Reset Types before running the worklist algorithm + Ary oldTypes = new Ary<>(Type.class); // Used for asserts only + resetToTop(code,oldTypes); + unlinkStart(code); + + // OPTIMISTIC INTERPROCEDURAL SCCP + sccp(code, oldTypes); + + // SCCP can leave a recursive numeric component at BOTTOM with no + // arithmetic mode selected. Resolve the whole component from all + // available int/float evidence before IterPeeps consumes the types. + resolveNumericModes(code); + + if( !code._iter._work.isEmpty() ) + sccp(code, oldTypes); + + // After SCCP add everything changed to the worklist and see if + // peepholes can make more progress. + moveChangesToWorklist(code, oldTypes); + } + + // Reset Types before running the worklist algorithm + private static void resetToTop(CodeGen code, Ary oldTypes ) { + code._start.walk( x -> { + oldTypes.setX(x._nid,x._type); // Record original types for asserts + x._type = Type.TOP; // Reset all Nodes to TOP + code._iter.add(x); // Visit everybody at least once; most Nodes produce better than TOP + return null; + } ); + } + + + // If wholeWorld, remove the Start inputs to functions - they are not + // called unless they get linked (hence go dead and can be removed). If + // not wholeWorld, keep only public methods (and not the std lib). + private static void unlinkStart(CodeGen code) { + // Keep the Start & Start.ProjMem alive even as we remove all uses + for( CompUnit cu : code._compunits.values() ) { + ProjNode mem = cu._start.proj(1); + if( mem!=null ) mem.keep(); + } + + // Iterate the Stop-of-Stops + for( Node stop : code._stop._inputs ) { + for( int i=0; i=0; fidx = XInt.next(fidxs,fidx) ) { + // unlinkStart deliberately leaves uncalled functions in a + // temporarily dead limbo. A newly discovered call revives them; + // do not use the cleanup lookup which removes dead linker entries. + FunNode fun = code._link(fidx); + // null here means an external function; i.e. this Call + // calls to an outside library and all its arguments escape. + if( fun != null && !call.linked(fun) ) { + call.link( fun ); + code._iter.add(fun); + code._iter.addAll(fun._outputs); + } + } + } + + // Start added some FIDXs, the same functions now can be reached from start + // and called by anybody. Add the CG edges. + private static void linkStart( CodeGen code, TypeTuple tt ) { + TypeMem tmem = (TypeMem)tt._types[1]; + int[] fidxs = tmem._escFs; + if( XInt.isHigh(fidxs) ) { + // A scalar BOTTOM can flow into memory while compiling an error + // program. It exposes all public functions, not private fields. + for( FunNode fun : code._linker ) + if( fun != null && code.publicFIDX(fun.sig().fidx()) ) + linkStart(code,fun); + return; + } + for( int fidx = XInt.next(fidxs,0); fidx >=0; fidx = XInt.next(fidxs,fidx) ) { + FunNode fun = code._linker.atX(fidx); + if( fun==null ) assert code._externFunc.containsKey(fidx); + else linkStart(code,fun); + } + } + + // Function is now reachable by unknown callers through Start. The + // default inputs must therefore use conservative declared types. + private static void linkStart( CodeGen code, FunNode fun ) { + assert !fun.isDead(); + StartCUNode start = fun._compunit._start; + if( !fun.unknownCallers() ) { + assert !start.isDead(); + code.keepInCompUnit(fun); + // Function is reachable by any *remote* caller who gets the pointer! + fun.insertDef(1,start); + code._iter.add(fun); + for( Node p : fun._outputs ) + if( p instanceof ParmNode parm ) { + Node defalt = parm._idx==1 ? start.proj(1) : ConstantNode.seed(parm._declaredType).peephole(); + parm.insertDef(1,defalt); + code.add(parm); + } + } + } + + + // The core SCCP algorithm + private static void sccp( CodeGen code, Ary oldTypes ) { + // Iter workList is empty at this point + // OPTIMISTIC PASS + Node n; + int count = 0; // Debug counter + while( (n = code._iter._work.pop()) != null ) { + if( n.isDead() ) continue; + count++; + Type oval = n._type, nval = n.compute(); + if( oval == nval ) continue; + assert oval.isa(nval) : "Non-monotonic SCCP: "+n.getClass().getSimpleName()+"#"+n._nid+" "+n+" old="+oval+" new="+nval; // Types start high and always fall + Type pesiVal = oldTypes.atX(n._nid); + assert pesiVal==null || nval.isa(pesiVal) : "SCCP below pessimistic type: "+n.getClass().getSimpleName()+"#"+n._nid+" "+n+" pessimistic="+pesiVal+" new="+nval; // Never fall worse than the pessimistic pass + n._type = nval; + + // Now we have a series of stanzas where we lazily create the Call + // Graph - adding graph edges between every call site and called + // function, including external (unknown) callers. These edges are + // not in from the start because they are *too many*; we would get + // O(n^2) edges. So all along we have been treating them as + // "virtual" CFG edges - a FunNode taking a Start input is treated + // as if every call on the planet might call it. Now we make these + // CG edges *concrete*, adding them back as we discover that a call + // calls a particular function. + + // If a TFP adds a new function input to a call, link to the new + // Fun. This adds new edges to the graph allowing argument values + // to flow from calls into functions. The added edges are + // effectively a refined Call Graph. + if( nval instanceof TypeFunPtr tfp ) { + for( Node use : n._outputs ) + if( !use._type.isHigh() && use instanceof CallNode call && call.fptr() == n ) + linkCG(code,tfp,call); + } + + // Link a Call which becomes alive. Like above, adds new Call + // Graph edges to the graph. + if( n instanceof CallNode call && nval == Type.CONTROL && call.fptr()._type instanceof TypeFunPtr ) + linkCG(code,call.tfp(),call); + + // If an otherwise-dead function pointer escapes, any future linked + // caller might find and call it. Force the function to be alive + // and called by Start. + if( n instanceof StartNode ) + linkStart(code,(TypeTuple)n._type); + + // Since n._type changed, visit all output neighbors + code._iter.addAll(n._outputs); + n.moveDepsToWorklist(code._iter); + // Quadratic (expensive) small-step assert. + assert !CodeGen.expensiveAssert(count) || worklistCheck(code); + } + } + + // Quadratic (expensive) fixed-point assert + public static boolean fixedPointCheck(CodeGen code) { + return fixedPointCheck(code,code._stop); + } + public static boolean fixedPointCheck(CodeGen code, Node root) { + root.walk( x -> { + Type xval = x.compute(); + assert xval == x._type; + return null; + }); + return true; + } + + // Quadratic (expensive) small-step assert for SCCP, where changed nodes + // may already be on the worklist. + private static boolean worklistCheck(CodeGen code) { + code._midAssert = true; + Node root = code._start; + root.walk( x -> { + Type xval = x.compute(); + assert xval == x._type || (x._type.isa( xval ) && code._iter._work.on( x )) + : "Non-monotonic or missed SCCP work: "+x.getClass().getSimpleName()+"#"+x._nid+" "+x+" old="+x._type+" new="+xval; + return null; + }); + code._midAssert = false; + return true; + } + + // After SCCP add everything changed to the worklist and see if peepholes + // can make more progress. + private static void moveChangesToWorklist(CodeGen code, Ary oldTypes) { + code._start.walk( x -> { + assert x.compute() == x._type; // Hit the fixed point + assert x._nid >= oldTypes._len || x._type.isa(oldTypes.at(x._nid)); // Hit at least the bottom-up type + code.add(x); + return null; + }); + } + + // Resolve arithmetic modes which are ambiguous only because they belong to + // a recursive component. Float wins because integer operands can be + // converted to float. + private static void resolveNumericModes(CodeGen code) { + final BitSet visited = new BitSet(); + code._stop.walk(n -> { + if( n instanceof ModeNode modeNode && modeNode.mode()==0 ) { + visited.clear(); + visited.set(n._nid); // Inspect inputs, not Bool's integer result + int mode = 0; + for( int i=1; i { return modeNode.mode(); } + case ModeNode modeNode -> {} + case PhiNode phi -> {} + case ReturnNode ret -> {} + case ProjNode proj when proj.in(0) instanceof CallEndNode cend -> { + for( int i=1; i { return 0; } + } + for( int i=1; i WORK = new Ary<>(CompUnit.class); + + + // Return a compilation unit (cached or new). If new, it does not necessarily + // need to be compiled. + public static CompUnit makeCUnit( CodeGen code, CompUnit par, String name ) throws IOException { + int idx=name.indexOf('/'); + if( idx>=0 ) return makeCUnit(code,makeCUnit(code,par,name.substring(0,idx)),name.substring(idx+1)); + String fname=par==null ? name : par._fname+"/"+name; + CompUnit cu=code._compunits.get(fname); + if( cu==null ) { + String cname=par==null ? name : par._cname+"."+name; + code._compunits.put(fname,cu=new CompUnit(par,fname,cname,name)); + } + return cu; + } + + + // Parse a top-level string, used for e.g. testing + static void parseSource( CodeGen code, String src ) { + parseSource(code,"Test",src); + } + + // Parse a top-level string with an explicit compilation-unit name. + static void parseSource( CodeGen code, String fname, String src ) { + CompUnit cunit = new CompUnit(fname,src); + code._compunits.put(cunit._fname,cunit); + parseAll(code,cunit); + } + + // module_root/.../src.smp is source file name. + static void parsePath(CodeGen code, String fname) { + try { + // Split and build the parent-path down to the source file. + CompUnit cu = makeCUnit( code, null, fname ); + // Compile this CompUnit + parseAll(code,cu); + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + + private static void enqueue(CompUnit cu) { + if( cu._par!=null ) enqueue(cu._par); + if( cu._start==null && WORK.find(cu)==-1 ) WORK.add(cu); + } + + private static void parseAll(CodeGen code, CompUnit cunit) { + WORK.clear(); // Cleanup from prior tests + // Prime the pump with the first CompUnit + enqueue(cunit); + // Work through all unparsed compilation units and parse + while( !WORK.isEmpty() ) { + CompUnit cu = WORK.remove(0); + if( cu._src==null ) + throw new RuntimeException("Missing source file '"+cu._fname+".smp'"); + parseOne(code,cu); + } + + // Many types will have been declared cyclically either internally or + // across multiple compilation units. + + // Close over all recursive types, and upgrade TYPES + Ary ary = new Ary<>(TypeStruct.class); + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct ts ) + ary.add(ts); + TypeStruct[] tss = Type.closeOver(ary.asAry(),Parser.TYPES); + for( TypeStruct ts : tss ) + Parser.TYPES.put(ts._name,ts); + for( CompUnit cu : code._compunits.values() ) + cu._clz = (TypeStruct)Parser.TYPES.get(Parser.addClzPrefix(cu._cname)); + + // Walk over all Nodes, and upgrade the internal constants to the + // closed-over types. + code._stop.walk( (Node n) -> { + n.upgradeType(Parser.TYPES); + if( n instanceof FRefNode fref ) { + boolean alloc = fref._name.endsWith("."); + String clzName = alloc + ? fref._name.substring(0,fref._name.length()-".".length()).intern() + : fref._name; + TypeStruct clz = (TypeStruct)Parser.TYPES.get(clzName); + if( clz == null ) + return null; + Node con; + if( alloc ) { + Field ctor = clz.field(""); + if( ctor == null || !(ctor._t instanceof TypeFunPtr tfp) ) { + String name = clzName.startsWith(Parser.CLZ) + ? clzName.substring(Parser.CLZ.length()) : clzName; + int dot = name.lastIndexOf('.'); + throw Parser.error("Unknown struct type '"+(dot==-1 ? name : name.substring(dot+1))+"'",fref._loc); + } + FunNode fun = code.link(tfp); + if( fun == null ) return null; + con = new FunPtrNode(tfp,code._start,fun.ret()).peephole(); + } else { + // Class pointer constant: + TypeMemPtr clzptr = TypeMemPtr.make((byte)2,clz,true); + con = code.con(clzptr); + } + // Replace and optimize + fref.addDef(con); + code.add(fref); + } + // Adds sort by NIDs, which just got shuffled + if( n instanceof AddNode add ) + code.add(add); + return null; + } ); + + // The complete public interface is now known. Keep this independent + // of later graph/linker liveness so SCCP computations remain local and + // monotonic. + code.freezePublicInterface(); + } + + // Parse one Simple source code file. Add all the FRefs produced to the worklist. + private static void parseOne( CodeGen code, CompUnit cunit ) { + cunit.setStart(code); + // Parse a source file, return missing external references + Ary frefs = code.P.parse(cunit); + + // Find all missing external references, or complain. This can trigger more parses. + while( !frefs.isEmpty() ) { + FRefNode fref = frefs.remove(0); + // Symbol not found in source code (or we would not be here). + // Search the module for the fref + try { + CompUnit xcunit = findCUnitModule(code,cunit,fref._name); + if( xcunit != null ) { + // Replace the FRef with the discovered class name. + fref._name = Parser.addClzPrefix(xcunit._cname); + cunit.addDep(xcunit); + } else { + throw new RuntimeException("Undefined name '" + fref._name +"'"); + } + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + addRequiredTypes(code,cunit); + } + + private static void addRequiredTypes(CodeGen code, CompUnit cunit) { + LinkedHashSet required = new LinkedHashSet<>(); + code._stop.walk(n -> { + TypeStruct ts = CodeGen.requiredStruct(n); + if( ts!=null && ts._fref ) required.add(ts._name); + return null; + }); + for( String typeName : required ) { + try { + CompUnit xcunit = findRequiredType(code,cunit,typeName); + if( xcunit != null ) + cunit.addDep(xcunit); + else + assert false : "expected to find required type"; + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + } + + private static CompUnit findRequiredType(CodeGen code, CompUnit cunit, String typeName) throws IOException { + for( CompUnit cu = cunit; cu != null; cu = cu._par ) { + String symbol = typeName.startsWith(cu._cname+".") + ? typeName.substring(cu._cname.length()+1) + : typeName; + CompUnit xcunit = findCUnitModule(code,cu,symbol); + if( xcunit != null ) + return xcunit; + } + return null; + } + + + // Symbol not found in source code (or we would not be here). + // Search the module for the fref + public static CompUnit findCompUnitOrThrow(CodeGen code, CompUnit cu, String symbol) { + CompUnit cu2 = findCompUnit(code,cu,symbol); + if( cu2 != null ) return cu2; + throw new RuntimeException("Undefined name '" + symbol +"'"); + } + + public static CompUnit findCompUnit(CodeGen code, CompUnit cu, String symbol) { + // Symbol not found in source code (or we would not be here). + // Search the module for the fref + try { + CompUnit local = findCUnitModule(code,cu,symbol); + if( local != null ) + return local; + return null; + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + + // Find the symbol in module, starting from cunit. This searches up the nested + // class chain and sideways 1 step, so that any prefix of the class chain can + // be skipped as a shortcut. Symbol can have dotted names, which then go + // down into classes. e.g. + // A/X/Y exists. + // A/B contains X.Y, which is a shortcut for A.X.Y + + // However if A/B contains "Y", the search would be for A.B.Y then A.Y both of + // which do not exist. + private static CompUnit findCUnitModule( CodeGen code, CompUnit cunit, String symbol ) throws IOException { + if( cunit == null ) { + if( !symbol.equals("sys") || !CompUnit.source("sys").exists() ) return null; + CompUnit sys=makeCUnit(code,null,"sys"); + enqueue(sys); + return sys; + } + + // Check self + if( cunit._name.equals(symbol) ) + return cunit; + + // If symbol is itself nested (has embedded dots), move the leading part + // over to the CompUnit path. So e.g.: + // CompUnit{A.B}, Symbol="X.Y" + // becomes + // CompUnit{A.B.X}, Symbol="Y" + int dotIdx = symbol.indexOf("."); + if( dotIdx != -1 ) { + String pre = symbol.substring(0,dotIdx); + String rest = symbol.substring(dotIdx+1 ); + String fname = cunit._fname + "/" + pre; + File f = CompUnit.source(fname); + if( f.exists() ) { + CompUnit sub = makeCUnit(code,cunit,pre); + // enqueue also parses parents: their initialization must precede a child. + CompUnit dcunit = findCUnitModule(code,sub,rest); + if( dcunit != null ) + return dcunit; // Nested dot search found cunit + } + } + + // Look in mod / fname / name / symbol.smp + String fname = cunit._fname + "/" + symbol; + File f = CompUnit.source(fname); + if( f.exists() ) { + CompUnit sub = makeCUnit(code,cunit,symbol); + // If not seen before or out of date, parse the new nested cunit + if( sub._clz == null && WORK.find(sub) == -1 ) + enqueue(sub); + return sub; + } + + // Look up the scope + return findCUnitModule( code, cunit._par, symbol ); + } + + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java new file mode 100644 index 00000000..48319006 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java @@ -0,0 +1,14 @@ +package com.seaofnodes.simple.codegen; + +// This Node rip-relative encodings has sizes that vary by delta +public interface RIPRelSize { + // delta is the distance from the opcode *start* to the target start. Each + // hardware target adjusts as needed. X86 measures from the opcode *end* + // and small jumps are 2 bytes, so they'll need to subtract 2. + byte encSize(int delta); + + // Patch full encoding in. Used for both short and long forms. delta + // again is from the opcode *start*. + void patch( Encoding enc, int opStart, int opLen, int delta ); + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java new file mode 100644 index 00000000..ac792988 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java @@ -0,0 +1,780 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import java.util.Arrays; +import java.util.BitSet; +import java.util.IdentityHashMap; + +/** + * "Briggs/Chaitin/Click". + * Graph coloring. + *

+ * Every def and every use have a bit-set of allowed registers. + * Fully general to bizarre chips. + * Multiple outputs fully supported, e.g. add-with-carry or combined div/rem or "xor rax,rax" setting both rax and flags. + *

+ * Intel 2-address accumulator-style ops fully supported. + * Op-to-stack-spill supported. + * All addressing modes supported. + * Register pairs could be supported with some grief. + *

+ * Splitting instead of spilling (simpler implementation). + * Stack slots are "just another register" (tiny stack frames, simpler implementation). + * Stack/unstack during coloring with a marvelous trick (simpler implementation). + *

+ * Both bitset and adjacency list formats for the interference graph; one of + * the few times it's faster to change data structures mid-flight rather than + * just wrap one of the two. + *

+ * Liveness computation and interference graph built in the same one pass (one + * fewer passes per round of coloring). + *

+ * Single-register def or use live ranges deny neighbors their required + * register and thus do not interfere, vs interfering and denying the color + * and coloring time. Basically all function calls do this, but many oddball + * registers also, e.g. older div/mod/mul ops. (5x smaller IFG, the only + * O(n^2) part of this operation). + */ + +public class RegAlloc { + // Main Coloring Algorithm: + // Repeat until colored: + // Build Live Ranges (LRGs) + // - Intersect allowed registers + // If hard conflicts (LRGs with no allowed registers) + // - Pre-split conflicted LRGs, and repeat + // Build Interference Graph (IFG) + // - Self conflicts split now + // Color (remove trivial first then any until empty; reverse assign colors + // - If color fails: + // - - Split uncolorable LRGs + + // Top-level program graph structure + final CodeGen _code; + + public int _spills, _spillScaled; + + private boolean _done; + + // ----------------------- + // Live ranges with self-conflicts or no allowed registers + private final IdentityHashMap _failed = new IdentityHashMap<>(); + // Loop Phis get one chance to color after only cold self-conflict splits. + // If the same Phi self-conflicts again, split its hot def/backedge also. + private final IdentityHashMap _deferredLoopSelf = new IdentityHashMap<>(); + void fail( LRG lrg ) { + assert lrg.leader(); + _failed.put(lrg,""); + } + boolean success() { return _failed.isEmpty(); } + public boolean done() { return _done; } + + + // ----------------------- + // Map from Nodes to Live Ranges + private final IdentityHashMap _lrgs = new IdentityHashMap<>(); + short _lrg_num; + + // Define a new LRG, and assign n + LRG newLRG( Node n, FunNode fun ) { + LRG lrg = lrg(n); + if( lrg!=null ) return lrg; + lrg = new LRG(_lrg_num++, fun); + LRG old = _lrgs.put(n,lrg); assert old==null; + return lrg; + } + + // LRG for n + public LRG lrg( Node n ) { + LRG lrg = _lrgs.get(n); + if( lrg==null ) return null; + LRG lrg2 = lrg.find(); + if( lrg != lrg2 ) + _lrgs.put(n,lrg2); + return lrg2; + } + + // Find LRG for n.in(idx), and also map n to it + LRG lrg2( Node n, int idx ) { + LRG lrg = lrg(n.in(idx)); + return union(lrg,n); + } + + // Union any lrg for n with lrg and map to the union + LRG union( LRG lrg, Node n ) { + LRG lrgn = _lrgs.get(n); + LRG lrg3 = lrg.union(lrgn); + _lrgs.put(n,lrg3); + return lrg3; + } + + // Force all unified to roll up; collect live ranges + LRG[] _LRGS; + void unify() { + Ary lrgs = new Ary<>(LRG.class); + for( Node n : _lrgs.keySet() ) { + LRG lrg = lrg(n); + lrgs.setX(lrg._lrg,lrg); + } + _LRGS = lrgs.asAry(); + // Remove unified lrgs from failed set also + for( LRG lrg : _failed.keySet().toArray(new LRG[0]) ) { + if( !lrg.leader() ) { + LRG lrg2 = lrg.find(); + _failed.remove(lrg); + _failed.put(lrg2,""); + } + } + } + + + // Diagnostic lookup: do not compress leaders or rewrite the node mapping. + private LRG _lrg(Node n) { + LRG lrg = _lrgs.get(n); + if( lrg != null ) + while( lrg._leader != null ) lrg = lrg._leader; + return lrg; + } + + public short regnum( Node n ) { + LRG lrg = _lrg(n); + return lrg==null ? -1 : lrg._reg; + } + + // Printable register number for node n + String reg( Node n ) { + LRG lrg = _lrg(n); + return lrg==null ? null : reg(lrg); + } + String reg( LRG lrg ) { + // No register yet, use LRG + if( lrg._reg == -1 ) return "V"+lrg._lrg; + // Chosen machine register unless stack-slot and past RA + String[] regs = _code._mach.regs(); + if( lrg._reg < regs.length || _code._phase.ordinal() <= CodeGen.Phase.RegAlloc.ordinal() || lrg._fun==null ) + return RegMask.reg(regs,lrg._reg); + // Stack-slot past RA uses the frame layout logic + return "[rsp+"+lrg._fun.computeStackOffset(_code,lrg._reg)+"]"; + } + + // ----------------------- + RegAlloc( CodeGen code ) { _code = code; } + + public void regAlloc() { + // Insert callee-save registers + String[] regs = _code._mach.regs(); + long neverSave= _code._mach.neverSave(); + for( CFGNode bb : _code._cfg ) + if( bb instanceof FunNode fun ) { + ReturnNode ret = fun.ret(); + int len = Math.min(regs.length,64); + for( int reg=0; reg= 7 ) // Really expect to be done soon + throw new IllegalStateException("Register allocation made no progress after eight rounds"); + round++; + } + postColor(); // Remove no-op spills + _done = true; + } + + boolean verifyFunctionLocalEdges() { + for( CFGNode bb : _code._cfg ) + for( Node use : bb._outputs ) { + FunNode useFun = funOf(use); + for( int i=1; i ") + .append(use.uniqueName()).append("(").append(useFun.label()).append(") input ").append(idx) + .append(" def=").append(def).append(" use=").append(use).append("\n all uses:"); + for( Node out : def.outs() ) { + FunNode outFun = funOf(out); + sb.append("\n ").append(out.uniqueName()).append("(") + .append(outFun==null ? "?" : outFun.label()).append(")"); + } + return sb.toString(); + } + + private static boolean allowedCrossFunctionEdge(Node use, int idx, Node def) { + // Function metadata/control edges are not value live ranges. + if( use instanceof FunNode || use instanceof StopNode || use instanceof StopCUNode ) + return true; + // Return-linked function pointers carry function identity/lifetime, not + // an allocator value flowing from the callee body into this function. + if( use instanceof FunPtrNode && def instanceof ReturnNode ) + return true; + // Parm defaults and call-linked argument edges are the entry protocol; + // BuildLRG also skips Parm input 1. + if( use instanceof ParmNode ) + return true; + // Linked CallEnd -> Return edges should be gone before RegAlloc, but + // diagnose allocator value edges rather than call graph metadata here. + if( use instanceof CallEndNode && def instanceof ReturnNode ) + return true; + return false; + } + + private static FunNode funOf(Node n) { + if( n instanceof ParmNode parm ) return parm.fun(); + if( n instanceof ReturnNode ret ) return ret.fun(); + if( n instanceof FunNode fun ) return fun; + if( n instanceof CFGNode cfg ) return cfg.fun(); + CFGNode cfg = n.cfg0(); + return cfg==null ? null : cfg.fun(); + } + + private boolean graphColor(byte round) { + _failed.clear(); + _lrgs.clear(); + _lrg_num = 1; + _LRGS=null; + + return + // Build Live Ranges + BuildLRG.run(round,this) && // if no hard register conflicts + // Build Interference Graph + IFG.build(round,this) && // If no self conflicts or uncolorable + // Conservative coalesce copies + Coalesce.coalesce(round,this) && + // Color attempt + IFG.color(round,this); // If colorable + } + + // ----------------------- + // Split conflicted live ranges. + void split(byte round) { + + // In C2, all splits are handling in one pass over the program. Here, + // in the name of clarity, we'll handle each failing live range + // independently... which generally requires a full pass over the + // program for each failing live range. i.e., might be a lot of + // passes. + + // Sort, to avoid non-deterministic HashMap ordering + LRG[] splits = _failed.keySet().toArray(new LRG[0]); + Arrays.sort(splits, (x,y) -> x._lrg - y._lrg ); + for( LRG lrg : splits ) + split(round,lrg); + } + + // Split this live range, top level heuristic + boolean split( byte round, LRG lrg ) { + assert lrg.leader(); // Already rolled up + + if( lrg._selfConflicts != null ) + return splitSelfConflict(round,lrg); + + // Register mask when empty; split around defs and uses with limited + // register masks. + if( lrg._mask.isEmpty() ) { + if( lrg._1regDefCnt <= 1 && + lrg._1regUseCnt <= 1 && + (lrg._1regDefCnt + lrg._1regUseCnt) > 0 && + // A fixed-register clone with flexible uses must split at the uses. + !(lrg._1regDefCnt==1 && lrg._machDef.isClone() && lrg._1regUseCnt==0) ) + return splitEmptyMaskSimple(round,lrg); + // Repeated single-reg uses from a single def. Special for archs + // with more fixed regs. + if( !lrg._multiDef && lrg._1regDefCnt <= 1 && lrg._1regUseCnt > 2 ) + if( splitEmptyMaskByUse(round,lrg) ) + return true; + } + if( lrg._mask.isEmpty() && lrg._multiDef && lrg._1regUseCnt > 1 && lrg._1regDefCnt <= 1 ) { + if( splitEmptyMaskSingleUses(round,lrg) ) + return true; + } + + // Generic split-by-loop depth. + return splitByLoop(round,lrg); + } + + // Split live range with an empty mask. Specifically forces splits at + // single-register defs or uses and not elsewhere. + boolean splitEmptyMaskSimple( byte round, LRG lrg ) { + // Live range has a single-def single-register, and/or a single-use + // single-register. Split after the def and before the use. Does not + // require a full pass. + + // Compute def nOuts *before* replacing those uses with a split + int defNouts = ((Node)lrg._machDef).nOuts(); + // Split just after def + if( lrg._1regDefCnt==1 && !lrg._machDef.isClone() ) + // Force must-split, even if a prior split same block because register + // conflicts. Example: + // alloc + // V1/rax - forced by alloc + // alloc + // V2/rax - kills prior RAX + // st4 [V1],len - No good, must split around + insertAfterAndReplace( makeSplit("def/empty1",round,lrg), (Node)lrg._machDef, false/*true*/); + // Split just before use + if( lrg._machUse!=null && (lrg._1regUseCnt==1 || (lrg._1regDefCnt==1 && defNouts==1)) ) + insertBefore((Node)lrg._machUse,lrg._uidx,"use/empty1",round,lrg); + return true; + } + + // Popular single-def value: share a split among uses with compatible masks. + boolean splitEmptyMaskByUse( byte round, LRG lrg ) { + Node def = (Node)lrg._machDef; + // Snapshot distinct users: rewriting an input changes def's output list, + // and a call can use the same value in more than one argument. + Ary uses = new Ary<>(Node.class); + for( Node use : def._outputs ) + if( uses.find(use)==-1 ) uses.push(use); + + Ary rclass = new Ary<>(RegMask.class); + int ncalls=0; + for( Node use : uses ) + if( use instanceof MachNode mach ) { + // Sharing copies across several calls usually loses to their kills. + if( use instanceof CallNode && ++ncalls > 1 ) return false; + for( int i=1; i1 ) split.setDef(1,def); + for( Node use : uses ) + if( use instanceof MachNode mach ) + for( int i=1; i rclass, RegMask rmask ) { + for( int i=0; i x._nid - y._nid ); + + // First encounter with a loop Phi: try only cold splits this round. + // Remember the Phi across rounds so a persistent self-conflict forces + // the hot def/backedge splits on the next encounter. + IdentityHashMap deferred = new IdentityHashMap<>(); + for( Node def : conflicts ) { + if( def instanceof PhiNode phi && phi.region() instanceof LoopNode ) + deferLoopSelf(phi,deferred); + for( Node use : def._outputs ) + if( use instanceof PhiNode phi && phi.region() instanceof LoopNode && + phi._inputs.find(def)==2 ) + deferLoopSelf(phi,deferred); + } + + // For all conflicts + for( Node def : conflicts ) { + assert lrg(def)==lrg; // Might be conflict use-side + // Split before each use that extends the live range; i.e. is a + // Phi or two-address + for( int i=0; i loop.idepth() ) ) || + (use instanceof MachNode mach && mach.twoAddress()!=0 && use.in(mach.twoAddress())==def) ) + insertBefore( use, use._inputs.find(def), "use/self/use",round,lrg ); + } + // Split after the Phi and before its entry. Also isolate inputs + // shared with another Phi: otherwise the shared input keeps the + // conflicting live ranges joined, even after splitting the entry. + if( def instanceof PhiNode phi && !(def instanceof ParmNode) ) { + if( !deferred.containsKey(phi) ) { + SplitNode split = makeSplit("def/self",round,lrg); + insertAfterAndReplace(split,def,false); + if( split.nOuts()==0 ) + split.killOrdered(); + } + for( int i=1; i<(deferred.containsKey(phi) ? 2 : phi.nIns()); i++ ) + if( i==1 || sharedPhiInput(phi,i) ) + insertBefore(phi,i,"use/self/phi",round,lrg); + } + // Split before two-address ops which extend the live range + if( def instanceof MachNode mach && mach.twoAddress()!= 0 ) + insertBefore(def,mach.twoAddress(),"use/self/two",round,lrg); + } + return true; + } + + private void deferLoopSelf(PhiNode phi, IdentityHashMap deferred) { + if( !_deferredLoopSelf.containsKey(phi) ) { + _deferredLoopSelf.put(phi,""); + deferred.put(phi,""); + } + } + + + private static boolean sharedPhiInput(PhiNode phi, int i) { + for( Node use : phi.in(i)._outputs ) + if( use instanceof PhiNode && use!=phi ) return true; + return false; + } + + // Generic: split around the outermost loop with non-split def/uses. This + // covers both self-conflicts (once we split deep enough) and register + // pressure issues. + boolean splitByLoop( byte round, LRG lrg ) { + findAllLRG(lrg); + + // Find min loop depth for all non-split defs and uses. + long ld = (-1L<<32) | 9999; + for( Node n : _ns ) { + if( lrg(n)==lrg ) // This is a LRG def + ld = ldepth(ld,n,n.cfg0()); + // PhiNodes check all CFG inputs + if( n instanceof PhiNode phi ) { + for( int i=1; i>32); + + boolean emptyMulti = lrg._mask.isEmpty() && lrg._multiDef && lrg._1regDefCnt <= 1 && + (lrg._1regUseCnt > 1 || _ns._len > 5); + // Multi-def empty-mask conflicts need the hot/deep side isolated from + // fixed register uses. Ordinary pressure splits keep the old shallow + // boundary behavior. + for( Node n : _ns ) { + if( n instanceof SplitNode split && !split.isNonStackSplit() && min!=max ) continue; // Ordinary splits already reach stack storage. + if( n.isDead() ) continue; // Some Cloneable went dead by other spill changes + // If this is a 2-address commutable op (e.g. AddX86, MulX86) and the rhs has only a single user, + // commute the inputs... which chops the LHS live ranges' upper bound to just the RHS. + if( n instanceof MachNode mach && lrg(n)==lrg && mach.twoAddress()==1 && mach.commutes() && n.in(2).nOuts()==1 ) + n.swap12(); + + if( lrg(n)==lrg && // This is a LRG def + // At loop boundary, or splitting in inner loop + (min==max || (emptyMulti ? n.cfg0().loopDepth() >= max : n.cfg0().loopDepth() <= min)) ) { + // Cloneable constants will be cloned at uses, not after def + if( !(n instanceof MachNode mach && mach.isClone()) && + // Single user is already a split adjacent + !(n.nOuts()==1 && n.out(0) instanceof SplitNode split && sameBlockNoClobber(split) ) ) + // Split after def in min loop nest + insertAfterAndReplace( makeSplit("def/loop",round,lrg), n,true); + } + + // PhiNodes check all CFG inputs + if( n instanceof PhiNode phi && !(n instanceof ParmNode)) { + for( int i=1; i= max : phi.region().cfg(i).loopDepth() <= min)) && + // and not around the backedge of a loop (bad place to force a split, hard to remove) + !(phi.region() instanceof LoopNode && i==2 && (phi.in(i) instanceof PhiNode pp && pp.region()==phi.region())) ) + // Split before phi-use in prior block + insertBefore(phi,i, "use/loop/phi",round,lrg); + + } else { + // Others check uses + for( int i=1; i= max : n.cfg0().loopDepth() <= min)) ) + // Split before in this block + insertBefore( n, i, "use/loop/use", round,lrg, false ); + } + } + } + return true; + } + + private boolean lrgSame(Node x, LRG lrg) { + LRG xlrg = lrg(x); + while( xlrg==null && x instanceof SplitNode ) + xlrg=lrg(x = x.in(1)); + return xlrg==lrg; + } + + private static long ldepth( long ld, Node n, CFGNode cfg ) { + // Ordinary splits already reach stack storage; register-only transfers do not. + if( n instanceof SplitNode split && !split.isNonStackSplit() ) return ld; + // Collect min/max loop depth + int min = (int)ld; + int max = (int)(ld>>32); + int d = cfg.loopDepth(); + // if n will lower the min loop and is in the tail end of the loop + // header, splitting "around" the loop will not help. Treat n as being + // in the loop. + if( d < min ) { + if( cfg.uctrl() instanceof LoopNode loop && loop.entry()==cfg ) { + for( int i=cfg.nOuts()-2; i>=0; i-- ) { + Node out = cfg.out(i); + if( n==out ) + { d = loop.loopDepth(); break; } // Treat n as being "in the loop" + if( !((out instanceof MachNode mach && mach.isClone()) || out instanceof SplitNode ) ) + break; // Treat b as "normal", out of loop + } + } + } + + // lower min, raise max, and re-fold + min = Math.min(min,d); + max = Math.max(max,d); + return ((long)max<<32) | min; + } + + // Find all members of a live range, both defs and uses + private final Ary _ns = new Ary<>(Node.class); + void findAllLRG( LRG lrg ) { + _ns.clear(); + int wd = 0; + if( lrg._machDef != null ) _ns.push((Node)lrg._machDef); + if( lrg._machUse != null ) _ns.push((Node)lrg._machUse); + while( wd < _ns._len ) { + Node n = _ns.at(wd++); + if( lrg(n)!=lrg ) continue; + for( Node def : n._inputs ) + if( lrg(def)==lrg && _ns.find(def)== -1 ) + _ns.push(def); + for( Node use : n._outputs ) + if( _ns.find(use)== -1 ) + _ns.push(use); + } + for( Node n : _ns ) assert !n.isDead(); + } + + void insertBefore(Node n, int i, String kind, byte round, LRG lrg, boolean skip) { + Node def = n.in(i); + // Phi splitting may visit the synthetic never-exit arm. + if( def._type==Type.TOP ) return; + // Effective block for use + CFGNode cfg = n instanceof PhiNode phi ? phi.region().cfg(i) : n.cfg0(); + // Use-side RegMask, if available + RegMask umask = n instanceof MachNode mach ? mach.regmap(i) : null; + // Def is a split ? + if( skip && def instanceof SplitNode ) { + boolean singleReg = umask!=null && umask.size1(); + // Same block, multiple registers, split is only used by n, + // assume this is good enough and do not split again. + if( cfg==def.cfg0() && def.nOuts()==1 && !singleReg ) + return; + } + makeSplit(def,kind,round,lrg,umask).insertBefore(n, i); + // Skip split-of-split same block + if( skip && def instanceof SplitNode && cfg==def.cfg0() ) + n.in(i).setDefOrdered(1,def.in(1)); + } + void insertBefore(Node n, int i, String kind, byte round, LRG lrg) { + insertBefore(n,i,kind,round,lrg,true); + } + + // Replace uses of `def` with `split`, and insert `split` immediately after + // `def` in the basic block. + public void insertAfterAndReplace( Node split, Node def, boolean must ) { + split.insertAfter(def); + if( split.nIns()>1 ) split.setDef(1,def); + for( int j=def.nOuts()-1; j>=0; j-- ) { + Node use = def.out(j); + if( use==split ) continue; // Skip self + // Can we avoid a split of a split? 'this' split is used by + // another split in the same block. + if( !must && use instanceof SplitNode split2 && sameBlockNoClobber(split2) ) + continue; + int idx = use._inputs.find(def); + use.setDefOrdered(idx,split); + if( j < def.nOuts() ) j++; + } + } + + private Node makeSplit( Node def, String kind, byte round, LRG lrg, RegMask umask ) { + // Clone simple constants if possible + Node split = def instanceof MachNode mach && mach.isClone() && (umask==null || mach.outregmap().overlap(umask)) + ? mach.copy() + : _code._mach.split(lrg,kind,round); + _lrgs.put(split,lrg); + return split; + } + private SplitNode makeSplit( String kind, byte round, LRG lrg ) { + SplitNode split = _code._mach.split(lrg,kind,round); + _lrgs.put(split,lrg); + return split; + } + + + // ----------------------- + // POST PASS: Remove empty spills that biased-coloring made + private void postColor() { + int maxReg = -1; + for( CFGNode bb : _code._cfg ) { // For all ops + if( bb instanceof FunNode ) + maxReg = -1; // Reset for new function + // Compute frame size, based on arguments and largest reg seen + if( bb instanceof ReturnNode ret ) + ret.fun().computeFrameAdjust(_code,maxReg); + // Raise frame size by max stack args passed, even if ignored + if( bb instanceof CallEndMach cend ) + maxReg = Math.max(maxReg,cend._xslot); + + for( int j=0; j= 0; + } + } + } + + // A sole, adjacent use can read the source register directly. Nothing can + // clobber it in between; only the operand mask and two-address tie remain. + private boolean splitForward( CFGNode bb, int j, Node split, int reg ) { + if( split.nOuts()!=1 || j+1==bb.nOuts() ) return false; + Node use = split.out(0); + if( use!=bb.out(j+1) || use instanceof PhiNode || use instanceof CFGNode || + !(use instanceof MachNode mach) ) return false; + int i = use._inputs.find(split); + RegMask mask = mach.regmap(i); + return mask!=null && mask.test(reg) && mach.twoAddress()!=i; + } + + private boolean splitBypass( CFGNode bb, int j, Node lo, int defreg ) { + // Attempt to bypass split + Node hi = lo.in(1); + while( true ) { + if( !(hi instanceof SplitNode && lo.cfg0() == hi.cfg0()) ) + return false; + if( lrg(hi.in(1))._reg==defreg ) + break; + hi = hi.in(1); + } + // Walk backward from the later copy, including instructions without results. + for( int idx = j-1; bb.out(idx) != hi; idx--) { + Node n = bb.out(idx); + if( clobbers(n,defreg) ) + return false; // Clobbered + } + lo.setDefOrdered(1,hi.in(1)); + return true; + } + + + private boolean clobbers(Node n, int reg) { + LRG lrg = lrg(n); + return lrg!=null && (lrg._reg==reg || lrg._reg==-1 && lrg._mask.size1() && lrg._mask.firstReg()==reg) || + n instanceof MachNode mach && mach.killmap()!=null && mach.killmap().test(reg); + } + + public boolean sameBlockNoClobber( SplitNode split ) { + Node def = split.in(1); + CFGNode cfg = def.cfg0(); + if( cfg != split.cfg0() ) return false; // Not same block + // Get multinode head + Node def0 = def instanceof ProjNode ? def.in(0) : def; + int defreg = lrg(def)._reg; + if( defreg == -1 ) defreg = lrg(def)._mask.firstReg(); + if( defreg == -1 ) return false; // no allowed registers -> clobbered + for( int idx = cfg._outputs.find(split) -1; idx >= 0; idx-- ) { + Node n = cfg.out(idx); + if( n==def0 ) return true; // No clobbers + if( lrg(n) == lrg(def) ) return false; // Self conflict + if( clobbers(n,defreg) ) + return false; // Clobbered + } + throw Utils.TODO("Should not reach here"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMask.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMask.java new file mode 100644 index 00000000..9cb9ec16 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMask.java @@ -0,0 +1,121 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +/** RegMask + * A "register mask" - 1 bit set for each allowed register. In addition, + * "stack slot" registers may be allowed, effectively making the set infinite. + *

+ * For smaller and simpler machines it suffices to make such masks an i64 or + * i128 (64- or 128-bit integers), and this presentation is by far the better + * way to go... if all register allocations can fit in this bit limitation. + * The allocator will need bits for stack-based parameters and for splits + * which cannot get a register. For a 32-register machine like the X86, add 1 + * for flags - gives 33 registers. Using a Java `long` has 64 bits, leaving + * 31 for spills and stack passing. This is adequate for nearly all + * allocations; only the largest allocations will run this out. However, if + * we move to a chip with 64 registers we'll immediately run out, and need at + * least a 128 bit mask. Since you cannot *return* a 128 bit value directly + * in Java, Simple will pick up a `RegMask` class object. +*/ +public class RegMask { + + long _bits0, _bits1; + + private static final RegMask EMPTY = new RegMask(0L); + public static final RegMask FULL = new RegMask(-1L); + + public RegMask(int bit) { + if( bit < 64 ) _bits0 = 1L<> reg)&1)==0; + if( reg < 128 ) return ((_bits1 >> reg)&1)==0; + return true; + } + + + public short firstReg() { + if( isEmpty() ) return -1; + return (short)(_bits0 != 0 + ? Long.numberOfTrailingZeros(_bits0) + : Long.numberOfTrailingZeros(_bits1)+64); + } + public short nextReg(short reg) { + if( reg < 63 ) { + long bits0 = _bits0 & (-1L << (reg+1)); + if( bits0!=0 ) return (short)Long.numberOfTrailingZeros(bits0); + } + long bits1 = reg < 64 ? _bits1 : reg < 127 ? _bits1 & (-1L << (reg-63)) : 0; + return bits1==0 ? -1 : (short)(64+Long.numberOfTrailingZeros(bits1)); + } + + boolean isEmpty() { return _bits0==0 && _bits1==0; } + + public boolean test( int reg ) { + return ((reg<64 ? (_bits0 >> reg) : (_bits1 >> (reg-64))) & 1) != 0; + } + + // checks if the 2 masks have at least 1 bit in common + public boolean overlap( RegMask mask ) { return (_bits0 & mask._bits0)!=0 || (_bits1 & mask._bits1)!=0; } + + // Defensive writable copy + public RegMaskRW copy() { return new RegMaskRW( _bits0, _bits1 ); } + + // Has exactly 1 bit set + public boolean size1() { + return !isEmpty() && (((_bits0 & -_bits0)==_bits0 && _bits1==0) || + ((_bits1 & -_bits1)==_bits1 && _bits0==0)); + } + + // Cardinality + public short size() { return (short)(Long.bitCount(_bits0)+Long.bitCount(_bits1)); } + + @Override public String toString() { return toString(new SB()).toString(); } + public SB toString(SB sb) { + Machine mach = CodeGen.CODE._mach; + if( _bits0==0 && _bits1==0 ) return sb.p("[]"); + sb.p("["); + String[] regs = mach.regs(); + for( int i=0; i<64; i++ ) + if( ((_bits0 >> i)&1) != 0 ) + sb.p(reg(regs,i)).p(","); + for( int i=0; i<64; i++ ) + if( ((_bits1 >> i)&1) != 0 ) + sb.p(reg(regs,i+64)).p(","); + return sb.unchar().p("]"); + } + + public static String reg(String[] regs, int i) { + return isource.length ) + return name+": error: "+message+" (source unavailable at byte offset "+pos+")"; + + int line=1, start=0; + for( int i=0; istart && source[end-1]=='\r' ) end--; + int point=Math.min(pos,end); + String before=new String(source,start,point-start,StandardCharsets.UTF_8); + String text=new String(source,start,end-start,StandardCharsets.UTF_8); + int column=before.codePointCount(0,before.length())+1; + SB sb=new SB().p(name).p(':').p(line).p(':').p(column).p(": error: ").p(message).p("\n "); + int width=0, caret=0; + for( int i=0; i objects = new LinkedHashMap<>(); + final ArrayList relocations = new ArrayList<>(); + final String prefix; + + public ArrayList entries() { + ArrayList entries=new ArrayList<>(objects.values()); + entries.sort(Comparator.comparingInt((ObjectData obj) -> obj.alignment).reversed()); + return entries; + } + + StaticData(Encoding enc) { + this.enc=enc; + prefix=enc._code.entryClinitName()+"$DATA$"; + } + + static boolean isClass(Type t) { + return t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name); + } + + static CompUnit owner(CodeGen code,TypeStruct ts) { + return owner(code,ts._name.substring(Parser.CLZ.length())); + } + static CompUnit owner(CodeGen code,String name) { + if(name==null) return null; + CompUnit owner=null; + for( CompUnit cu : code._compunits.values() ) + if( cu._cname!=null && (name.equals(cu._cname) || name.startsWith(cu._cname+".")) && + (owner==null || cu._cname.length()>owner._cname.length()) ) owner=cu; + return owner; + } + + ObjectData object(Type t) { + Object key=isClass(t) ? ((TypeStruct)t)._name + : t instanceof TypeFunPtr fp ? functionSymbol(fp) : t; + ObjectData old=objects.get(key); + if( old!=null ) return old; + boolean clz=isClass(t); + if( clz && Parser.TYPES.get(((TypeStruct)t)._name) instanceof TypeStruct canonical ) t=canonical; + CompUnit owner=clz ? owner(enc._code,(TypeStruct)t) + : t instanceof TypeFunPtr fp ? owner(enc._code,enc._code._fidxs.owner(fp.fidx())) : null; + String symbol=clz ? ((TypeStruct)t)._name.substring(Parser.CLZ.length())+".$class" + : t instanceof TypeFunPtr fp ? functionSymbol(fp) : prefix+objects.size(); + ObjectData obj=new ObjectData(t,symbol,!clz && (!(t instanceof TypeStruct ts) || ts.isConstant()), + owner!=null && owner._src==null); + objects.put(key,obj); + // Register before following fields: class objects may refer to each other. + if( !obj.external && t instanceof TypeStruct ts ) + for( Field f : ts._fields ) + if( !f._extern && f._t.isConstant() ) target(f._t); + return obj; + } + + private static boolean pointerConstant(TypeMemPtr ptr) { + return ptr.notNull() && (isClass(ptr._obj) || (ptr._obj.isConstant() && (!ptr._obj.isAry() || ptr._obj.isConAry()))); + } + + private void target(Type t) { + if( t instanceof TypeMemPtr ptr && pointerConstant(ptr) ) object(ptr._obj); + if( t instanceof TypeFunPtr fp && fp.notNull() && + enc._code.link(fp)==null && !enc._code._externFunc.containsKey(fp.fidx()) ) + // A dead function still needs a unique non-null identity for comparisons. + object(fp); + } + + void write() { + ArrayList refs=new ArrayList<>(enc._bigCons.values()); + refs.sort(Comparator.comparingInt(r -> r._op._nid)); + for( Encoding.Relo ref : refs ) object(ref._t); + // Definitions belong to their compilation unit, even if every local use folded. + ArrayList units=new ArrayList<>(enc._code._compunits.values()); + units.sort(Comparator.comparing(cu -> cu._cname==null ? "" : cu._cname)); + for( CompUnit cu : units ) if( cu._src!=null && cu._clz!=null ) object(cu._clz); + for( ObjectData obj : entries() ) { + if( obj.external ) continue; + BAOS bits=obj.readOnly ? enc._cpool : enc._sdata; + Encoding.padN(1<0xffff_ffffL) ) + throw new IllegalArgumentException("Function address outside 32 bits: "+address); + BAOS bits=rel.object.readOnly ? enc._cpool : enc._sdata; + int off=rel.object.offset+rel.offset; + for( int i=0; i<(1<>>(8*i)); + } + } + + int address(ObjectData obj,int cpool,int sdata) { + if( obj.external ) throw new IllegalArgumentException("Unlinked class object "+obj.symbol); + return obj.offset+(obj.readOnly ? cpool : sdata); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java new file mode 100644 index 00000000..9b4b0995 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java @@ -0,0 +1,57 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.Ary; + +// Reject invalid optimized programs before lowering erases their source-level +// obligations. Queue surviving CLInits during the same walk for Lower's Iter. +abstract class TypeCheck { + static void run(CodeGen code) { + // Check for class-init cycles first; such a cycle can prevent other + // things from resolving types. The dependency graph is temporary. + String clzInitCycle = new ClassInitDependencies(code).check(); + if( clzInitCycle != null ) + throw Parser.error(clzInitCycle,null); + + Ary errs = new Ary<>(Node.class); + code._stop.walk( n -> { + if( n.err() != null ) + errs.add(n); + if( n instanceof CLInitNode ) code.add(n); + return null; + }); + + if( !errs.isEmpty() ) { + // Prefer an originating error over a secondary error caused by a + // BOTTOM input. If all errors are downstream-poisoned, retain + // the old first-error behavior. + Node min = null; + for( Node n : errs ) + if( noBottomInputs(n) ) + { min = n; break; } + if( min==null ) + min = errs.at(0); + for( Node n : errs ) + if( n!=min && + noBottomInputs(n) && + ((n instanceof MemOpNode nmop && + min instanceof MemOpNode mmop && + nmop._loc.before(mmop._loc)) || + (n instanceof CFGNode ncfg && + min instanceof CFGNode mincfg && + ncfg.idepth() < mincfg.idepth())) ) + min = n; + if( code._obs != null ) code._obs.error(min, min.err().getMessage()); + throw min.err(); + } + } + + private static boolean noBottomInputs(Node n) { + for( Node def : n._inputs ) + if( def!=null && !(def instanceof FldOffNode) && def._type==Type.BOTTOM ) + return false; + return true; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/AddNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/AddNode.java new file mode 100644 index 00000000..8835208b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -0,0 +1,179 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +public class AddNode extends ArithNode { + @Override boolean allowFloat() { return true; } + public AddNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + public AddNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs, mode); } + + + @Override public String label() { return "Add"; } + @Override public String op() { return "+"; } + + @Override long doOp( long x, long y ) { return x + y; } + @Override double doOp(double x, double y ) { return x + y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + // Fold ranges like {0-1} + {2-3} into {2-4}. + if( !overflow(x._min,y._min) && + !overflow(x._max,y._max) ) + return TypeInteger.make(x._min+y._min,x._max+y._max); + return TypeInteger.BOT; + } + + @Override + public Node idealize () { + // Settle the one-shot arithmetic mode before applying family-specific + // algebraic identities. + if( _mode==0 ) + return super.idealize(); + + Node lhs = in(1); + Node rhs = in(2); + if( rhs instanceof AddNode add && add.err()!=null ) + return null; + Type t2 = rhs._type; + + // Add of 0. We do not check for (0+x) because this will already + // canonicalize to (x+0) + if( t2 == TypeInteger.ZERO && lhs._type.isa(compute()) ) + return lhs; + + // Add of same to a multiply by 2 + if( lhs==rhs && _mode==1 ) + return new ShlNode(null,lhs,CodeGen.CODE.con(1)); + + // 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),_mode); + + // 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 copy(copy(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)) + if( spine_cmp(lhs,rhs,this) ) return swap12(); + Node n = phiCon(this,true); + return n == null ? super.idealize() : n; + } + + // 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.nIns()>=2 && 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( addDep(lhs.in(2))._type.isConstant() && rhs._type.isConstant() ) + return copy(lhs.in(1),copy(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 copy(copy(lhs.in(1),rhs).peephole(),lhs.in(2)); + + return super.idealize(); + } + + // Rotation is only valid for associative ops, e.g. Add, Mul, And, Or, Xor. + // Do we have ((phi cons)|(x + (phi cons)) + con|(phi cons)|(op y con)) ? + // 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); + if( rhs._type==TypeInteger.TOP ) return null; + // 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; + if( lphi.region().nIns() <=2 ) return null; // Phi is collapsing + + // 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,_mode); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/AndNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/AndNode.java new file mode 100644 index 00000000..a737b703 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/AndNode.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class AndNode extends ArithNode { + public AndNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + + + @Override public String op() { return "&"; } + @Override long doOp( long x, long y ) { return x & y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + // Sharpen allowed bits if either value is narrowed + long mask = x.mask() & y.mask(); + return mask < 0 ? TypeInteger.BOT : TypeInteger.make(0,mask); + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + + Type t1 = lhs._type; + Type t2 = rhs._type; + if( !(t1 instanceof TypeInteger && t2 instanceof TypeInteger t2i) ) + return null; // Malformed, e.g. (17 & 3.14) + + // And of -1. We do not check for (-1&x) because this will already + // canonicalize to (x&-1). We do not check for zero, because + // the compute() call will return a zero already. + if( t2i.isConstant() && t2i.value()==-1 && forceInt(lhs) ) + 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 super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new AndNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ArithNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ArithNode.java new file mode 100644 index 00000000..094b927f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ArithNode.java @@ -0,0 +1,208 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +public abstract class ArithNode extends Node implements ModeNode { + // Source location for late reported errors + Parser.Lexer _loc; + // Mode is 0 for unknown, 1 for int, 2 for flt + byte _mode; + + public ArithNode( Parser.Lexer loc, Node lhs, Node rhs) { super(null, lhs, rhs); _loc = loc; } + public ArithNode( Parser.Lexer loc, Node lhs, Node rhs, byte mode) { this(loc,lhs,rhs); _mode=mode; } + + abstract String op(); + + @Override protected ExprPrinter _print1(ExprPrinter p) { + return p.open().n(in(1)).p(op()).n(in(2)).close(); + } + + abstract long doOp(long x, long y); + abstract TypeInteger doOp(TypeInteger x, TypeInteger y); + double doOp(double x, double y) { throw Utils.TODO(); } + + @Override public byte mode() { return _mode; } + @Override public Node setMode(byte mode) { + assert _mode==0 && (mode==1 || mode==2); + unlock(); // _mode participates in GVN hash and equality + _mode = mode; + return this; + } + static byte mode( Type t1, Type t2 ) { + if( t1 instanceof TypeFloat || t2 instanceof TypeFloat ) return 2; + if( t1 instanceof TypeInteger && t2 instanceof TypeInteger ) return 1; + if( t1 instanceof TypeMemPtr && t2 instanceof TypeMemPtr ) return 1; + if( t1 instanceof TypeFunPtr && t2 instanceof TypeFunPtr ) return 1; + return 0; + } + + // Int-only ops, such as &, | and shifts, can collapse to one input. If + // that input, or the live arms of a Phi input, are still mode-polymorphic + // ops, settle them before returning the value so the replacement remains + // at least as narrow as the int-only op. + static boolean forceInt(Node n) { + if( n instanceof ModeNode mode && mode.mode()==0 ) { + mode.setMode((byte)1); + n.setType(n.compute()); + // Mode changes remove n from GVN even if the caller's remaining + // conditions prevent replacing the int-only operation by n. + CodeGen.CODE.add(n); + } + return true; + } + + // Check that a settled mode agrees with all data inputs. Takes the Node + // instead of a varargs list both to avoid allocation and to keep this ready + // to become shared mode-aware Node behavior later. + static Parser.ParseException nodeErr(Node n, Parser.Lexer loc, String op, byte mode, boolean allowFloat) { + Type bad = null; + for( int i=1; i mode==1 ? null : t; + case TypeMemPtr ti -> mode==1 ? null : t; + case TypeFunPtr ti -> mode==1 ? null : t; + case TypeFloat tf -> mode==2 ? null : t; + default -> t; + }; + if( bad != null ) break; + } + boolean unsupportedFloat = false; + if( !allowFloat ) + for( int i=1; i= 2 && !lhs.inProgress() && + // Disallow with self-looping phi; these will collapse + (lhs.nIns() > 2 && lhs.in(2)!=lhs && rhs.nIns() > 2 && rhs.in(2)!=rhs) ) { + Node r = lhs.region(); + if( r==rhs.region() && !(r instanceof FunNode) && + // For loops, the backedge *must* fold away, lest we unroll forever + (!(r instanceof LoopNode) || isCon(lhs,rhs,2)) ) { + // Profit check: only 1 instance of `this` will remain, all the + // others will fold to constants. For loops, *all* must fold away + int cnt=0; + for( int i=1; i _print1(ExprPrinter p) { + return p.open().n(in(1)).p(op()).n(in(2)).close(); + } + + @Override + public TypeInteger compute() { + Type t1 = in(1)._type; + Type t2 = in(2)._type; + if( t1==null || t2==null ) + return BOOL; + // Exactly equals? + if( t1.isHigh() || t2.isHigh() ) + return (TypeInteger)BOOL.dual(); + if( in(1)==in(2) ) + // LT fails, both EQ and LE succeed + return this instanceof LT ? FALSE : TRUE; + byte mode = _mode==0 ? ArithNode.mode(t1,t2) : _mode; + if( mode==0 ) + return BOOL; + // Handle integer compares + if( mode==1 ) { + if( t1 instanceof TypeInteger i1 && + t2 instanceof TypeInteger i2 ) + return doOp(i1,i2); + return BOOL; + } + // Floating compare. Mixed inputs are converted in idealize(). + if( t1 instanceof TypeFloat f1 && + t2 instanceof TypeFloat f2 && + f1.isConstant() && f2.isConstant() ) + return doOp(f1.value(), f2.value()) ? TRUE : FALSE; + return BOOL; + } + + TypeInteger doOp(TypeInteger t1, TypeInteger t2) { throw Utils.TODO(); } + boolean doOp(double lhs, double rhs) { throw Utils.TODO(); } + public boolean isFloat() { assert _mode!=0; return _mode==2; } + @Override public byte mode() { return _mode; } + @Override public Node setMode(byte mode) { + assert _mode==0 && (mode==1 || mode==2); + unlock(); // _mode participates in GVN hash and equality + _mode = mode; + return this; + } + + @Override + public Node idealize() { + // Compare of same + if( in(1)==in(2) ) + return this instanceof LT ? CodeGen.CODE.ZERO : ConstantNode.make(TRUE); + // Can we decide int vs flt? + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==null || t2==null ) + return null; + if( _mode==0 ) { + byte mode = ArithNode.mode(t1,t2); + if( mode!=0 ) return setMode(mode).init(); + } + if( _mode==2 ) { + if( t1 instanceof TypeInteger ) return copy(new ToFloatNode(in(1)).peephole(),in(2)); + if( t2 instanceof TypeInteger ) return copy(in(1),new ToFloatNode(in(2)).peephole()); + } + + + // 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 || in(1)._nid > in(2)._nid ) + // Equals sorts by NID otherwise: non.high == non.low becomes non.low == non.high + return new EQ(in(2),in(1)); + } + // Equals X==0 becomes a !X + if( (in(2)._type == ZERO || in(2)._type == Type.NIL) ) + 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; + } + + @Override public boolean eq( Node n ) { return _mode==((BoolNode)n)._mode; } + @Override public int hash() { return _mode; } + @Override public Parser.ParseException err() { + return ArithNode.nodeErr(this,null,op(),_mode,!(this instanceof ULT)); + } + + public static class EQ extends BoolNode { + public EQ(Node lhs, Node rhs) { super(lhs,rhs); } + public EQ(Node lhs, Node rhs, byte mode) { super(lhs,rhs, mode); } + + public String op() { return "=="; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( i1==i2 && i1.isConstant() ) return TRUE; + if( i1._max < i2._min || i1._min > i2._max ) return FALSE; + return BOOL; + } + @Override boolean doOp(double lhs, double rhs) { return lhs==rhs; } + Node copy(Node lhs, Node rhs) { return new EQ(lhs,rhs,_mode); } + } + + public static class NE extends BoolNode { + public NE(Node lhs, Node rhs) { super(lhs,rhs); } + public NE(Node lhs, Node rhs, byte mode) { super(lhs,rhs,mode); } + + public String op() { return "!="; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( i1==i2 && i1.isConstant() ) return TRUE; + if( i1._max < i2._min || i1._min > i2._max ) return FALSE; + return BOOL; + } + @Override boolean doOp(double lhs, double rhs) { return lhs!=rhs; } + Node copy(Node lhs, Node rhs) { return new NE(lhs,rhs,_mode); } + } + + public static class LT extends BoolNode { + public LT(Node lhs, Node rhs) { super(lhs,rhs); } + public LT(Node lhs, Node rhs, byte mode) { super(lhs,rhs, mode); } + + public String op() { return "<" ; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( i1._max < i2._min ) return TRUE; + if( i1._min >= i2._max ) return FALSE; + return BOOL; + } + @Override boolean doOp(double lhs, double rhs) { return lhs= 0 ) return FALSE; + return BOOL; + } + Node copy(Node lhs, Node rhs) { return new ULT(lhs,rhs); } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java new file mode 100644 index 00000000..df9e4fff --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java @@ -0,0 +1,265 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.util.Utils; + +import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * 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. + // Directly visible by design. Empty sets share EMPTY; splitAlias performs + // copy-on-first-write and removes the node from GVN before mutation. + public BitSet _aliases; + + public BulkMemPhiNode(String label, Node... inputs) { + this(label,EMPTY,inputs); + } + + private BulkMemPhiNode(String label, BitSet aliases, Node... inputs) { + super(label, inputs); + _aliases = aliases; + } + + public BulkMemPhiNode(BulkMemPhiNode phi) { + super(phi,phi._label); + _aliases = phi._aliases.isEmpty() ? EMPTY : (BitSet)phi._aliases.clone(); + } + + public BulkMemPhiNode(RegionNode r, Node sample) { + super(r,sample); + _aliases = EMPTY; + } + + public boolean isSplit(int alias) { return _aliases.get(alias); } + + + @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)==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 ParmNode p -> { assert p._idx==1; yield 0; } + case ProjNode proj -> 0; + case ConstantNode con -> { + assert !(con._con instanceof TypeMem tmem && tmem._alias != 1); + yield 0; + } + case MemMergeNode mmm -> missingAlias(mmm); + case MemOpNode mem -> unsplit(mem._alias); + case BulkMemPhiNode bulk -> missingAlias(bulk); + default -> throw Utils.TODO("Should not reach here"); + }; + } + + // 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(use); + return switch(use) { + case ScopeNode scope -> 0; + case ParmNode parm -> 0; + case MemMergeNode mmm -> missingAlias(mmm); + case BulkMemPhiNode bulk -> missingAlias(bulk); + case MemOpNode mem -> unsplit(mem._alias); + case MemPhiNode phi -> unsplit(phi._alias); + case EscapeNode esc -> esc.pub()==this ? unsplit(esc.fld()._alias) : 0; + default -> { + assert use instanceof ReturnNode || use instanceof CallNode || use instanceof CLInitNode + : "Unexpected bulk-memory user "+use.getClass().getSimpleName()+"#"+use._nid+": "+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 explicit MemMerge slice still covered by this Phi. A user's slice + // may point to this Phi after a precise Phi folds; splitting is still valid. + private int missingAlias( MemMergeNode mmm ) { + 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); + mmm.init(); + // Slicing changes representation, not the set of values in memory. + // Newly exposed precise inputs can carry conservative bulk escape + // summaries; do not let those invent escapes absent from this Phi. + TypeMem old = (TypeMem)_type; + TypeMem mem = (TypeMem) mmm._type; + mmm._type = TypeMem.make(mem._alias,mem._t,mem._one,mem._clz, + mem._final,old._escFs,old._escAs); + 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.BOT.makeFrom(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/chapter-modules/src/main/java/com/seaofnodes/simple/node/CFGNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CFGNode.java new file mode 100644 index 00000000..4f225fbe --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CFGNode.java @@ -0,0 +1,303 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +import java.util.BitSet; +import java.util.IdentityHashMap; + +/** Control Flow Graph Nodes + *

+ * CFG nodes have a immediate dominator depth (idepth) and a loop nesting + * depth(loop_depth). + *

+ * idepth is computed lazily upon first request, and is valid even in the + * Parser, and is used by peepholes during parsing and afterward. + *

+ * loop_depth is computed after optimization as part of scheduling. + * + * Start - Block head; has constants and trailing Funs + * Region - Block head; has Phis. Includes Fun and Parms. + * CallEnd - Block head; followed by CProj and Proj; also linked RPC Parms + * CProj - Block head + * + * If - Block tail; followed by CProj same block + * Call - Block tail; followed by CallEnd and linked Funs + * New/intrinsic - If followed by CProj: block tail else mid-block; followed by Proj + * + */ +public abstract class CFGNode extends Node { + + public CFGNode(Node... nodes) { super(nodes); } + public CFGNode(CFGNode cfg) { + super(cfg); + if( cfg != null ) { + _idepth = cfg._idepth; + _idepthVersion = cfg._idepthVersion; + _ltree = cfg._ltree; + _pre = cfg._pre; + } + } + + public CFGNode cfg(int idx) { return (CFGNode)in(idx); } + + // Block head is Start, Region, CProj, but not e.g. If, Return, Stop + public boolean blockHead() { return false; } + + // Get the one control following; error to call with more than one e.g. an + // IfNode or other multi-way branch. + public CFGNode uctrl() { + CFGNode c = null; + for( Node n : _outputs ) + if( n instanceof CFGNode cfg ) + { assert c==null; c = cfg; } + return c; + } + + // Used by the encoding / final BB layout + public CFGNode uctrlSkipEmpty() { + CFGNode x = this, y; + while( x.nOuts() == 1 && (y=x.uctrl())!=null && !(y instanceof CallNode) ) // Skip empty blocks + x = y; + return x; + } + + // ------------------------------------------------------------------------ + /** + * Immediate dominator tree depth, used to approximate a real IDOM during + * parsing where we do not have the whole program, and also peepholes + * change the CFG incrementally. + *

+ * See {@link ...} + */ + public char _idepth; + public char _idepthVersion; + public int idepth() { return validIDepth() ? _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"; + _idepthVersion = CodeGen.CODE.iDepthVersion(); + return _idepth = (char)depth; + } + + final boolean validIDepth() { return _idepth!=0 && _idepthVersion==CodeGen.CODE.iDepthVersion(); } + + + // Return the immediate dominator of this Node and compute dom tree depth. + public CFGNode idom(Node dep) { return cfg(0); } + public final CFGNode idom() { return idom(null); } + + // Return the LCA of two idoms + public CFGNode domLCA(CFGNode rhs, Node dep) { + if( rhs==null ) return this; + CFGNode lhs = this; + while( lhs != rhs ) { + if( lhs==null || rhs==null ) return null; + int comp = lhs.idepth() - rhs.idepth(); + if( comp >= 0 ) lhs = (dep==null ? lhs : dep.addDep(lhs)).idom(); + if( comp <= 0 ) rhs = (dep==null ? rhs : dep.addDep(rhs)).idom(); + } + return lhs; + } + + // Safe read of the control input of a data node. + public static CFGNode safeCFG(Node n) { + if( n==null ) return null; + if( !(n.in(0) instanceof CFGNode cfg) ) return null; + return cfg._safeCFG(); + } + private CFGNode _safeCFG() { + return switch(this) { + case CLInitNode init when init.bypass() -> init.cfg0()._safeCFG(); + case CallNode call when call.cend()._folding -> call.cfg0()._safeCFG(); + case CallEndNode cend when cend._folding -> cend.cfg(1)._safeCFG(); + case FunNode fun when fun._folding -> fun.cfg(1)._safeCFG(); + case ReturnNode ret when ret.fun()._folding -> ret.cfg0()._safeCFG(); + default -> this; + }; + } + + // True when this node dominates `sub` in the current idom tree. + public boolean dominates(CFGNode sub, Node dep) { + for( CFGNode cfg=sub; cfg!=null; cfg=cfg.idom() ) + if( cfg == this ) + return true; + else if( dep != null ) dep.addDep(cfg); + return false; + } + + public static CFGNode deepest(CFGNode a, CFGNode b) { + if( a == null ) return b; + if( b == null ) return a; + return b.idepth() > a.idepth() ? b : a; + } + + public static CFGNode earlyCFG(Node n) { + return earlyCFG(n,new IdentityHashMap<>(),new BitSet()); + } + + public static CFGNode earlyCFG(Node n, IdentityHashMap cache, BitSet active) { + if( n == null ) return null; + CFGNode cached = cache.get(n); + if( cached != null ) return cached; + if( active.get(n._nid) ) + return safeCFG(n); + active.set(n._nid); + CFGNode early = null; + for( Node x : n._inputs ) + early = deepest(early,defCFG(x,cache,active)); + active.clear(n._nid); + cache.put(n,early); + return early; + } + + private static CFGNode defCFG(Node n, IdentityHashMap cache, BitSet active) { + if( n == null || n.isConst() ) return null; // no impact on early cfg + return switch( n ) { + case PhiNode phi -> phi.region(); + case CFGNode cfg -> cfg; + case ProjNode projNode -> safeCFG( n ); + default -> earlyCFG( n, cache, active ); + }; + } + + public boolean sameFun(CFGNode that) { + FunNode fun = fun(); + return fun != null && fun == that.fun(); + } + + // Find nearest enclosing FunNode + public FunNode fun() { + CFGNode cfg = this; + while( !(cfg instanceof FunNode fun) ) { + if( cfg==null ) return null; + cfg = cfg.idom(); + } + return fun; + } + + // ------------------------------------------------------------------------ + // Loop nesting + public LoopNode loop() { return _ltree._head; } + public int loopDepth() { return _ltree==null ? 0 : _ltree.depth(); } + + public LoopTree _ltree; + public int _pre; // Pre-order numbers for loop tree finding + public static class LoopTree { + public LoopTree _par; + public final LoopNode _head; + public int _depth; + LoopTree(LoopNode head) { _head = head; } + @Override public String toString() { return label(_head); } + public int depth() { + return _depth==0 ? (_par==null ? 0 : (_depth = _par.depth()+1)) : _depth; + } + } + + // ------------------------------------------------------------------------ + // Tag all CFG Nodes with their containing LoopNode; LoopNodes themselves + // also refer to *their* containing LoopNode, as well as have their depth. + // Start contains the function bodies at depth 0; real loops add depth. + public void buildLoopTree( Ary funs, StopNode stop) { + _ltree = stop._ltree = CodeGen.CODE.XCTRL._ltree = new LoopTree((StartNode)this); + Ary work = new Ary<>(FunNode.class); + + BitSet post = new BitSet(); + int pre = 1; + _pre = pre++; + for( FunNode fun : funs ) { + if( fun!=null && !fun.isDead() && !fun._type.isHigh() ) { + work.push(fun); + if(fun.in(1) instanceof StartCUNode start && start._pre==0 ) { + start._pre = pre++; + start._ltree = start.cfg(1)._ltree = _ltree; + post.set(start._nid); + } + }; + } + + for( int i=0; i work ) { + // Pre-walked? + if( _pre!=0 ) return pre; + _pre = pre++; + // Seed the function's outer tree before walking its body. Synthetic + // exits can be added before the post-order walk reaches the entry. + if( this instanceof FunNode ) _ltree = stop._ltree; + if( this instanceof ReturnNode ) { + _ltree = fun._ltree; + post.set(_nid); + return pre; // Stop/Start links describe the outside world, not a loop. + } + // Pre-walk + for( Node use : _outputs ) + if( use instanceof CFGNode usecfg ) { + if( skip( usecfg ) ) { + if( usecfg._pre==0 && usecfg instanceof FunNode fun2 ) work.add( fun2 ); + } else + pre = usecfg._bltWalk( pre, use instanceof FunNode fuse ? fuse : fun, stop, post, work ); + } + + // Post-order work: find innermost loop + LoopTree inner = null, ltree; + for( Node use : _outputs ) { + if( !(use instanceof CFGNode usecfg) ) continue; + if( skip(usecfg) ) continue; + if( usecfg._type == Type.XCONTROL || // Do not walk dead control + usecfg._type == TypeTuple.IF_NEITHER ) // Nor dead IFs + continue; + // Child visited but not post-visited? + if( !post.get(usecfg._nid) ) { + // Must be a backedge to a LoopNode then + ltree = usecfg._ltree = new LoopTree((LoopNode)usecfg); + } else { + // Take child's loop choice, which must exist + ltree = usecfg._ltree; + // If falling into a loop, use the target loop's parent instead + if( ltree._head == usecfg ) { + if( ltree._par == null ) + // This loop never had an If test choose to take its + // exit, i.e. it is a no-exit infinite loop. + ltree._par = ltree._head.forceExit(fun,stop)._ltree; + ltree = ltree._par; + } + } + // Sort inner loops. The decision point is some branch far removed + // from either loop head OR either backedge so requires pre-order + // numbers to figure out innermost. + if( inner == null ) { inner = ltree; continue; } + if( inner == ltree ) continue; // No change + LoopTree outer = ltree._head._pre > inner._head._pre ? inner : ltree; + inner = ltree._head._pre > inner._head._pre ? ltree : inner; + inner._par = outer; + } + // Set selected loop + if( inner!=null ) + _ltree = inner; + // Tag as post-walked + post.set(_nid); + return pre; + } + + public boolean skip(CFGNode usecfg) { + // Only walk control users that are alive. + // Do not walk from a Call to linked Fun's. + return usecfg instanceof XCtrlNode || + (this instanceof CallNode && usecfg instanceof FunNode) || + (this instanceof ReturnNode && usecfg instanceof CallEndNode); + } + + public static String label( CFGNode target ) { + return switch(target) { + case StartNode start -> start.label(); + case FunNode fun -> fun.label(); + case LoopNode loop -> "LOOP"+target._nid; + default -> "L"+target._nid; + }; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CLInitNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CLInitNode.java new file mode 100644 index 00000000..0a0de3cf --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CLInitNode.java @@ -0,0 +1,165 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CompUnit; +import com.seaofnodes.simple.type.*; +import java.util.*; + +// Track class initializers - the code just after this point expects the named +// class to be initialized. If this code path dies, then the dependency dies +// also - dead code does not force class init. +// +public class CLInitNode extends CFGNode implements MultiNode { + public Parser.Lexer _loc; // For error reporting + + // Inputs: control, whole memory, class pointer, loaded $init flag + // Results: control, whole memory, known-true flag alias memory. + // For field references, the ptr might be the dst clz ptr or not. + public CLInitNode(Parser.Lexer loc, Node ctrl, Node mem, Node ptr, Node ready) { + super(ctrl,mem,ptr,ready); + _loc = loc; + } + + + public CLInitNode(CLInitNode init) { + super(init); + _loc = init._loc; + } + + public Node mem() { return in(1); } + public Node ptr() { return in(2); } + public Node ready() { return in(3); } + + // The field/alias can be unresolved until forward references are loaded. + public Field flag() { + return ptr()._type instanceof TypeMemPtr tmp && Parser.startsClzPrefix(tmp._obj._name) + ? tmp._obj.field("$init") : null; + } + @Override public String label() { return "CLInit"; } + + // Best effort dst compunit, based on ptr - or null + public CompUnit cu() { + if( !(ptr()._type instanceof TypeMemPtr ptr && Parser.startsClzPrefix(ptr._obj._name)) ) + return null; + // Convert "class:sys.ary" into "sys/ary" and lookup CompUnit + String fname = ptr._obj._name.substring(6).replace(".","/"); + return CodeGen.CODE._compunits.get(fname); + } + + @Override protected ExprPrinter _print1(ExprPrinter p) { + FunNode fun = fun(); + CompUnit cu = cu(); + return p.p("CLInit ").p(fun._name).p(" -> ").p(cu==null ? "---" : cu._cname); + } + + // Block head is Start, Region, CProj, but not e.g. If, Return, Stop + public boolean blockHead() { return true; } + + private boolean instance() { + return ptr()._type==Type.NIL || + (ptr()._type instanceof TypeMemPtr tmp && tmp._obj!=TypeStruct.BOT && + !tmp._obj.isHigh() && !tmp._obj._open && !tmp._obj._fref && + !Parser.startsClzPrefix(tmp._obj._name)); + } + + public boolean bypass() { return instance() || ready()._type==TypeInteger.TRUE; } + + @Override public Type compute() { + if( in(0)._type.isHigh() || ptr()._type.isHigh() ) + return TypeTuple.make(Type.XCONTROL,TypeMem.TOP,TypeMem.TOP); + if( instance() ) + return TypeTuple.make(in(0)._type,mem()._type,mem()._type); + Field flag = flag(); + Type slice = flag==null ? TypeMem.BOT : TypeMem.make(flag._alias,TypeInteger.TRUE); + // The initializer may write arbitrary public memory. Only its flag + // has a guaranteed value on the normal continuation. + Type bulk = ready()._type.isHigh() ? TypeMem.TOP : + ready()._type==TypeInteger.TRUE ? mem()._type : TypeMem.BOT; + return TypeTuple.make(in(0)._type,bulk,slice); + } + + @Override public Node pcopy(int idx, Node dep) { + dep.addDep(ptr()); + dep.addDep(ready()); + if( instance() ) return idx==0 ? in(0) : mem(); + if( ready()._type!=TypeInteger.TRUE ) return null; + if( idx==0 ) return in(0); + if( idx==1 ) return mem(); + // Already established on the incoming path; this is a memory fact, + // not a new store to the class object. + return CodeGen.CODE.con(((TypeTuple)_type)._types[2]); + } + + @Override public Node idealize() { + if( CodeGen.CODE._phase!=CodeGen.Phase.Lower || bypass() || in(0)._type.isHigh() ) return null; + if( CodeGen.CODE._midAssert ) return this; + // Until entry arguments become arrays, ordinary initializers receive zero. + return expand(CodeGen.CODE.con(TypeInteger.ZERO)); + } + + // Called only after cycle/type checking. The initialized arm skips the + // call; both arms rejoin with whole memory and a known-true flag slice. + private Node expand(Node arg) { + FunNode target = CodeGen.CODE.link(cu()); + assert target!=null && !target.isDead(); + keep(); + Node ctrl = in(0); + Node memory = mem(); + + // A child body may assume its parent has finished. Establish that + // requirement first, then test the child's flag in the resulting memory. + CompUnit parent = cu()._par; + if( parent!=null ) { + Node ptr = CodeGen.CODE.con(TypeMemPtr.make((byte)2,parent._clz,true)); + Field flag = parent._clz.field("$init"); + Node off = CodeGen.CODE.con(parent._clz.offset(parent._clz.find("$init"))); + Node parentReady = CodeGen.CODE.add(new LoadNode(_loc,"$init",flag._alias,ctrl,memory,ptr,off).init()); + CLInitNode init = CodeGen.CODE.add(new CLInitNode(_loc,ctrl,memory,ptr,parentReady).init()); + ctrl = CodeGen.CODE.add(new CProjNode(init,0,ScopeNode.CTRL).init()); + memory = CodeGen.CODE.add(new ProjNode(init,1,ScopeNode.MEM0).init()); + } + + Node childOff = CodeGen.CODE.con(cu()._clz.offset(cu()._clz.find("$init"))); + // Read the child's flag after establishing its parent's memory effects. + Node ready = CodeGen.CODE.add(new LoadNode(_loc,"$init",flag()._alias,ctrl,memory,ptr(),childOff).init()); + + IfNode iff = CodeGen.CODE.add(new IfNode(ctrl,ready).init()); + Node done = CodeGen.CODE.add(new CProjNode(iff,0,"True").init()); + Node cold = CodeGen.CODE.add(new CProjNode(iff,1,"False").init()); + + Node fptr = new FunPtrNode(target.sig(),CodeGen.CODE._start,target.ret()).peephole(); + CallNode call = CodeGen.CODE.add(new CallNode(_loc,cold,memory,ptr(),arg,fptr).init()); + CallEndNode end = new CallEndNode(call,TypeRPC.constant(CodeGen.CODE.rpc(_loc._cu._cname))); + // This is a lowered call, after interprocedural analysis. Do not add + // new parameter flows to a body already optimized by SCCP. + CodeGen.CODE.add(end.init()); + Node called = CodeGen.CODE.add(new CProjNode(end,0,ScopeNode.CTRL).init()); + Node after = CodeGen.CODE.add(new ProjNode(end,1,ScopeNode.MEM0).init()); + RegionNode region = CodeGen.CODE.add(new RegionNode(_loc,null,done,called).init()); + Node merged = CodeGen.CODE.add(new BulkMemPhiNode(ScopeNode.MEM0,region,memory,after).init()); + + // Replace the individual outputs; no tuple-pack node or second + // representation of CLInit is needed while the projections collapse. + for( Node use : _outputs.asAry() ) { + if( use==null ) continue; // Our temporary keep. + assert use instanceof Proj; + int idx = ((Proj)use).idx(); + use.subsume(idx==0 ? region : idx==1 ? merged : CodeGen.CODE.con(((TypeTuple)_type)._types[2])); + } + CodeGen.CODE.invalidateIDepthCaches(); + return unkeep(); // Iter removes this now-unused node. + } + + // Resolve a precise flag slice even when the parser did not yet know the + // class/alias and could only install the whole-memory projection. + static Node slice(Node mem, int alias, Node dep) { + if( !(mem instanceof ProjNode proj && proj._idx==1 && proj.in(0) instanceof CLInitNode init) ) + return mem; + dep.addDep(init.ptr()); + Field flag = init.flag(); + if( flag==null || flag._alias!=alias ) return mem; + return new ProjNode(init,2,Parser.memName(alias)).peephole(); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CProjNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CProjNode.java new file mode 100644 index 00000000..2bd48d92 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CProjNode.java @@ -0,0 +1,73 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeTuple; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class CProjNode extends CFGNode implements Proj { + + // Which slice of the incoming multipart value + public int _idx; + + // Debugging label + public String _label; + + public CProjNode(Node ctrl, int idx, String label) { + super(ctrl); + _idx = idx; + _label = label; + } + public CProjNode(CProjNode c) { super(c); _idx = c._idx; _label = c._label; } + + + @Override public String label() { return _label; } + + @Override protected ExprPrinter _print1(ExprPrinter p) { return p.p(_label); } + + @Override public boolean blockHead() { return true; } + + public CFGNode ctrl() { return cfg(0); } + + @Override + public Type compute() { + Type t = ctrl()._type; + return t instanceof TypeTuple tt ? tt._types[_idx] : (t.isHigh() ? Type.TOP : Type.BOTTOM); + } + + @Override + public Node idealize() { + if( ctrl()._type instanceof TypeTuple tt ) { + if( tt._types[_idx]==Type.XCONTROL ) + return CodeGen.CODE.XCTRL; // We are dead + if( ctrl() instanceof IfNode && tt._types[1-_idx]==Type.XCONTROL ) // Only true for IfNodes + return ctrl().in(0); // We become our input control + } + + // Flip a negating if-test, to remove the not + if( ctrl() instanceof IfNode iff && iff.pred() != null && addDep(iff.pred()) instanceof NotNode not ) + return new CProjNode(new IfNode(iff.ctrl(),not.in(1)).peephole(),1-_idx,_idx==0 ? "False" : "True"); + + // Copy of some other input + return ((MultiNode)ctrl()).pcopy(_idx,this); + } + + // Only called during basic-block layout, inverts a T/F CProj + public void invert() { + _label = _idx == 0 ? "False" : "True"; + _idx = 1-_idx; + } + + @Override + public boolean eq( Node n ) { return _idx == ((CProjNode)n)._idx; } + + @Override + int hash() { return _idx; } + + @Override public int idx() { return _idx; } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndMach.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndMach.java new file mode 100644 index 00000000..05707132 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndMach.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.util.SB; + +public class CallEndMach extends CallEndNode implements MachNode { + public CallEndMach( CallEndNode n ) { super(n); } + // MachNode specifics, shared across all CPUs + public int _xslot; + private RegMask _retMask; + private RegMask _kills; + public void cacheRegs(CodeGen code) { + // Return mask depends on TFP (either GPR or FPR) + _retMask = code._mach.retMask(call().tfp()); + // Kill mask is all caller-saves, and any mirror stack slots for args + // in registers. + RegMaskRW kills = (call().external() ? code._cCallerSave : code._callerSave).copy(); + // Start of stack slots + int maxReg = code._mach.regs().length; + // Incoming function arg slots, all low numbered in the RA + int fslot = fun()._maxArgSlot; + // Killed slots for this calls outgoing args + int xslot = call().external() + ? code._mach.maxArgSlot(call().tfp(),code._cCallingConv) + : code._mach.maxArgSlot(call().tfp()); + _xslot = (maxReg+fslot)+xslot; + for( int i=0; i _print1(ExprPrinter p) { + p.p("cend( "); + p.p( in(0) instanceof CallNode ? "Call, " : "----, "); + for( int i=1; i= 0; fidx = XInt.next(tfp.fidxs(),fidx) ) { + if( CodeGen.CODE.externFunc(fidx)!=null ) + externs++; + FunNode fun = CodeGen.CODE._link(fidx); + bound = bound.meet(fun==null ? tfp.ret() : addDep(fun).sig().ret()); + } + } + boolean hasExtern = externs != 0; + + // If we have an error-call, e.g. wrong args, then we never link. + // Pre-Opto this looks like a missing target fcn, and we assume it will + // appear later - meanwhile, we use a conservative approx of memory + // effects. + if( (nIns()-1)+externs < tfp.nfcns() ) { + // If during Opto, assume call won't be called and thus won't return anything. + // Lowered calls deliberately have no links into optimized bodies. + boolean opto = CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() && + CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Lower.ordinal(); + Type tmem = opto ? TypeMem.TOP : TypeMem.BOT; + Type ret = opto ? Type.TOP : tfp.ret(); + // Use the same widening before and after linking a callee. + return TypeTuple.make(Type.CONTROL,tmem,widenResult(ret,bound)); + } + + // A linked function for every concrete function + TypeTuple state = hasExtern + ? TypeTuple.make(Type.CONTROL,call.exposed(this),tfp.ret()) + : TypeTuple.STATE.dual(); + for( int i=1; i CallEnd -> Add -> Phi -> Return, without + // crossing ParmNode.compute's widening point. Widen here too, + // using the loop rule in PhiNode.compute. Keep constants and + // above-center ranges narrow. + if( ret instanceof TypeInteger ti && !ti.isHigh() && !ti.isConstant() ) + ret = ti.same_but_slightly_wider_than().join(bound); + return ret; + } + + @Override + public Node idealize() { + return null; + } + + public byte maybeInline() { + byte inline = inlineCandidate(); + if( CodeGen.CODE._midAssert ) + return inline; + _inline = inline; + if( _inline > 0 ) + doInline(); + + return _inline; + } + + // Check if candidate right now, sets no flags + private byte inlineCandidate() { + if( _inline < -2 ) return -2; // Never + if( _folding ) return -2; // No, and will collapse anyways + if( !(in(0) instanceof CallNode call) ) return -2; // malformed because mid-death + + if( nIns()!=2 ) return -1; // Not exactly 1 link, but can change + + // Have exactly 1 linked function + ReturnNode ret = (ReturnNode)in(1); + FunNode fun = ret.fun(); + + // Preserve initializer boundaries until post-Opto cycle validation. + if( fun.isClz() ) return -2; + + // Heuristic forced inlining turned off via name + if( fun._name != null && + fun._name.endsWith("_noInline") ) + return -2; // Never, stop asking + + // Disallow self-recursive inlining (loop unrolling by another name). + for( int i=1; i < fun.nIns(); i++ ) + if( fun.cfg(i).fun()==fun ) // Check for linked call input inside "fun" + { addDep(fun); return -1; } // No, but can lose the recursive edge + + // Things that might improve, but not now a candidate + Node fptr = call.fptr(); + // Constant Function is being called, and its not-null + if( !(fptr._type instanceof TypeFunPtr tfp && tfp.notNull() && tfp.isConstant()) ) + { addDep(fptr); return -1; } + + // Arguments are correct + if( call.nargs()!=tfp.nargs() || !goodArgs(call,tfp) ) + { addDep(fptr); return -1; } + + // Encouraged inlining because small size and constructor. + int maxSize = fun.inlineLimit(); + // A large parse can leave only a tiny initializer. Recognize that + // and await cleanup to shrink the body. + if( fun._approxUIDs >= maxSize ) { + if( !smallBody(fun) ) return -1; + } else if( fun.body().cardinality() >= maxSize ) // Expensive size check + return -1; // Try later (if cleanup makes the real body small enough) + + // Can be cloned but not trivial + if( fun.nIns() > 2 ) { addDep(fun); return 2; } + + // A trivial candidate right now + return 1; + } + + // Bounded, conservative recognition of straight-line initializer boilerplate. + // Walk uses from the entry and stop at Return, never following callers or + // constant inputs. This also avoids scanning sparse MemMerge input arrays. + // Watch only this small prefix, including an unsupported node that may fold. + private static final Node[] TINYBODY = new Node[15]; // Tiny worklist + private boolean smallBody(FunNode fun) { + ReturnNode ret = fun.ret(); + for( Node in : ret._inputs ) + if( in!=null ) addDep(in); + if( ret.ctrl()!=fun ) return false; + TINYBODY[0] = fun; + int len=1; + for( int i=0; iTINYBODY.length ) return false; // Too big + for( Node use : n._outputs ) { + if( use==null ) continue; + int j=0; + while( j 0; // Only called when possible to inline + + ReturnNode ret = (ReturnNode)in(1); + FunNode fun = ret.fun(); + CallNode call = call(); + assert fun.sig().isa(call.fptr()._type); + + // Clone first + if( _inline==2 ) { + assert !CodeGen.CODE._midAssert; // Triggered inlining + // Remove the existing function linkage + call.unlink_all(); + // Clone the function body + FunNode fun2 = fun.copyBody(); + // Call uses the unique new function + Node fptr2 = new FunPtrNode(fun2.sig(),CodeGen.CODE._start,fun2.ret()).peephole(); + call.setDef(call.nIns()-1,fptr2); + // Link to the new function + call.link(fun2); + fun = fun2; + } + + // Trivial inlining: call site calls a single function; single function + // is only called by this call site. + doTrivialInlining(fun); + } + + boolean goodArgs( CallNode call, TypeFunPtr tfp ) { + for( int i=0; i _print1(ExprPrinter p) { + String fname = name(); + if( fname == null ) p.n(fptr()); + else p.p(fname); + p.p("( "); + for( int i=2; i=0; fidx=XInt.next(tfp.fidxs(),fidx) ) + if( CodeGen.CODE.externFunc(fidx)!=null ) return true; + return false; + } + + // As in the escape chapter, native code may retain an argument even if + // this path never returns. StopCU reads this independently of CallEnd. + TypeMem exposed(Node dep) { + dep.addDepForwards(this); + dep.addDepForwards(fptr()); + if( _type!=Type.CONTROL || !external() ) return TypeMem.START; + Type input=dep.addDepForwards(mem())._type; + TypeMem escaped=input instanceof TypeMem m ? m : input.isHigh() ? TypeMem.TOP : TypeMem.BOT; + for( int i=2; i0 && out(0) instanceof CallEndNode cend ) { + assert _cend()==cend; + return cend; + } else { + assert _cend()==null; + return null; + } + } + private CallEndNode _cend() { + CallEndNode cend=null; + for( Node n : _outputs ) + if( n instanceof CallEndNode cend0 ) + { assert cend == null; cend = cend0; } + return cend; + } + + // Get the one control following; error to call with more than one e.g. an + // IfNode or other multi-way branch. + @Override public CFGNode uctrl() { return cend(); } + + + @Override + public Type compute() { + return ctrl()._type; + } + + @Override + public Node idealize() { + // Lowering may introduce calls after the call graph has been analyzed. + // These use the callee interface without reopening its parameter flow. + if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Lower.ordinal() ) return null; + CallEndNode cend = cend(); + if( cend==null ) return null; // Still building + + // Link: call calls target function. Linking makes the target FunNode + // point to this Call, and all his Parms point to the call arguments; + // also the CallEnd points to the Return. + Node progress = null; + if( fptr()._type instanceof TypeFunPtr tfp && tfp.nargs() == nargs() ) { + // If fidxs is negative, then infinite unknown functions + int[] fidxs = tfp.fidxs(); + if( !XInt.isHigh(fidxs) ) { + // Walk the bits and link + for( int fidx = XInt.next(fidxs,0); fidx > 0; fidx = XInt.next(fidxs,fidx) ) { + FunNode fun = CodeGen.CODE.link(fidx); + if( fun!=null && !fun._folding && !linked(fun) ) + progress = link(fun); + } + } + } + + return progress; + } + + // True if Fun is linked to this Call + public boolean linked( FunNode fun ) { + for( Node n : fun._inputs ) + if( n == this ) + return true; + return false; + } + + // Link so this calls fun + public Node link( FunNode fun ) { + assert !linked(fun); + CodeGen.CODE.add(fun).addDef(this); + for( Node use : fun._outputs ) + if( use instanceof ParmNode parm ) { + Node def = parm._idx==0 ? ConstantNode.seed(cend()._rpc).peephole() : arg(parm._idx); + CodeGen.CODE.add(parm).addDef(def); + // Linking can happen during post-SCCP inlining. Keep an RPC + // Parm's cached finite set synchronized with its newly added + // input instead of waiting for the next worklist visit. + if( parm._idx==0 && parm._type instanceof TypeRPC ) + parm._type = parm._type.meet(def._type); + } + // Call end points to function return + CodeGen.CODE.add(cend()).addDef(fun.ret()); + return this; + } + + // Unlink a single function + public void unlink( FunNode fun, int path ) { + assert linked(fun); + for( Node use : fun._outputs ) + if( use instanceof ParmNode ) + use.delDef(path); + fun.delDef(path); + CodeGen.CODE.add(fun); + CallEndNode cend = cend(); + ReturnNode ret = fun.ret(); + if( cend != null ) { + ret.keep(); + cend.delDef(cend._inputs.find(ret)); + CodeGen.CODE.add(cend); + ret.unkeep(); + } + assert !linked(fun); + } + + + // Unlink all linked functions + public void unlink_all() { + for( int i=0; i<_outputs._len; i++ ) + if( out(i) instanceof FunNode fun ) { + int path = fun._inputs.find(this); + unlink(fun,path); + i--; + } + } + + @Override + public Parser.ParseException err() { + if( fptr()._type == Type.BOTTOM ) + return null; // Wrong, but failing earlier + if( !(fptr()._type instanceof TypeFunPtr tfp) ) + return Parser.error("Expected a function, but found " + fptr()._type, _loc); + if( !tfp.notNull() ) + return Parser.error( "Might be null calling "+tfp, _loc); + if( nargs() != tfp.nargs() ) + return Parser.error( "Expecting "+tfp.nargs()+" arguments, but found "+nargs(), _loc); + + // Check for args + for( int i=0; i

+ * 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 TypeNode { + public ConstantNode( Type type ) { + this(type,false); + } + protected ConstantNode( Type type, boolean raw ) { + super(type,new Node[]{CodeGen.CODE._start}); + assert raw || !(type instanceof TypeFunPtr tfp && tfp.isConstant()) : + "Unique function identities require a Return-linked FunPtrNode"; + _type = type; + } + public ConstantNode( ConstantNode con ) { super(con); } + + + // Semantic constant construction through Lower. A unique internal + // function identity carries a lifetime edge to the function's Return. + public static Node make( Type type ) { + if( type==Type. CONTROL ) return new CtrlNode(); + if( type==Type.XCONTROL ) return new XCtrlNode(); + if( CodeGen.CODE._phase != null && + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.Lower.ordinal() ) + return raw(type); + if( type instanceof TypeFunPtr tfp && tfp.isConstant() ) { + FunNode fun = CodeGen.CODE._link(tfp); + if( fun != null ) + return new FunPtrNode(tfp,CodeGen.CODE._start,fun.ret()).init(); + // TODO: A singleton FIDX can also emerge after merging and + // guarding several function pointers. Preserve that identity + // even if its internal Return is no longer available. + return raw(type); + } + return new ConstantNode(type); + } + + // Exact construction for deserialization and graph-incomplete machine + // selection. No semantic classification, compute, idealize, or GVN. + public static ConstantNode raw( Type type ) { return new ConstantNode(type,true); } + public static ConstantNode raw( ConstantNode con ) { return new ConstantNode(con); } + + // Analysis input created from whole cloth, rather than a first-class + // runtime value. In particular, a constructor call descriptor does not + // own the constructor function; the class FunPtr does. + public static ConstantNode seed( Type type ) { return new ConstantNode(type,true); } + + @Override protected String repeatName() { + return _con==null || _con.toString().length()<=32 ? null : uniqueName(); + } + + @Override public String label() { return "Con"; } + @Override public Node copy() { return raw(this); } + + + @Override + protected ExprPrinter _print1(ExprPrinter p) { + if( _con instanceof TypeFunPtr tfp && tfp._isConstant() && tfp.notNull() ) { + FunNode fun = CodeGen.CODE._link(tfp); + if( fun!=null && fun._name != null ) + return p.p("{ ").p(fun._name).p("}"); + } + return p.p(_con==null ? "---" : _con.toString()); + } + + @Override public boolean isConst() { return true; } + @Override public Type compute() { return _con; } + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConvertNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConvertNode.java new file mode 100644 index 00000000..cdc890a7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConvertNode.java @@ -0,0 +1,101 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; + +// Convert a value to an authoritative destination type. The destination is +// known structurally from a declaration; the source family might sharpen later. +public class ConvertNode extends Node { + private Type _dst; + + public ConvertNode(Type dst, Node val) { + super(null,val); + // Folding this conversion would replace the Return-linked FunPtrNode + // with an unhooked ConstantNode. + assert !(dst instanceof TypeFunPtr && val instanceof FunPtrNode); + _dst = dst; + } + + public Type dst() { return _dst; } + public Node val() { return in(1); } + + @Override public String label() { return "Convert_"+_dst.str(); } + @Override protected ExprPrinter _print1(ExprPrinter p) { + return p.p(label()).open().n(val()).close(); + } + + // The declaration fixes the result family during pessimistic parsing. + // From SCCP onward, preserve dead inputs and stronger facts in that family. + @Override public Type compute() { + Type src = val()._type; + if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() ) { + if( src.isHigh() ) return Type.TOP; + if( (src.isa(_dst) && src.accessISA(_dst)) ) return src; + } + return _dst; + } + + @Override public Node idealize() { + Type src = val()._type; + if( src==Type.BOTTOM || src==Type.TOP ) return null; + if( (src.isa(_dst) && src.accessISA(_dst)) ) return val(); + + // Managed arrays are represented by an object pointer, while C wants + // the address of the first element. The representation cast itself is + // zero-code; the Add skips the array header. + if( _dst==TypeInteger.BOT && src instanceof TypeMemPtr tmp && tmp._obj.isAry() ) + return new AddNode(new PtrToIntNode(val()).peephole(),Parser.off(tmp._obj,"[]")); + + // Integer (and nil-as-zero) to floating point. + if( (src instanceof TypeInteger || src==Type.NIL) && _dst instanceof TypeFloat ) + return new ToFloatNode(val()); + + // Convert to i64 first, then apply the destination's integer width. + if( src instanceof TypeFloat && _dst instanceof TypeInteger ) + return new ConvertNode(_dst,new ToIntegerNode(val()).peephole()); + + // Narrow integers produce the declared sign/zero extension. + if( src instanceof TypeInteger && _dst instanceof TypeInteger dst ) { + if( dst._min==0 ) + return new AndNode(null,val(),con(dst._max)); + int shift = Long.numberOfLeadingZeros(dst._max)-1; + Node shf = con(shift); + if( shf._type==TypeInteger.ZERO ) return val(); + return new SarNode(null,new ShlNode(null,val(),shf.keep()).peephole(),shf.unkeep()); + } + + // Narrow f64 to f32. + if( src instanceof TypeFloat && _dst instanceof TypeFloat ) + return new RoundF32Node(val()); + + // Wait for more source information, or report an error after Opto. + return null; + } + + @Override boolean _upgradeType(HashMap TYPES) { + Type dst = _dst.upgradeType(TYPES); + if( dst==_dst ) return false; + unlock(); + _dst = dst; + return true; + } + + @Override public Parser.ParseException err() { + if( val()._type==Type.BOTTOM || + val()._type instanceof TypeNil src && src.nullable() && _dst instanceof TypeNil dst && dst.notNull() ) { + Node out = out(0); + if( out instanceof PhiNode phi ) out = phi.out(0); + if( out instanceof StoreNode st ) + return Parser.error( "'" + ((TypeMemPtr)st.ptr()._type)._obj._name + "' is not fully initialized, field '" + st._name + "' is only partially set in the constructor", null ); + } + return Parser.error("Type "+val()._type.str()+" is not of declared type "+_dst.str(),null); + } + + @Override public boolean eq(Node n) { return _dst==((ConvertNode)n)._dst; } + @Override int hash() { return _dst.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CtrlNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CtrlNode.java new file mode 100644 index 00000000..813e6fcb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CtrlNode.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; + +public class CtrlNode extends CFGNode { + public CtrlNode() { super(CodeGen.CODE._start); } + + @Override public String label() { return "Ctrl"; } + @Override protected ExprPrinter _print1(ExprPrinter p) { return p.p("Cctrl"); } + @Override public boolean isConst() { return true; } + @Override public Type compute() { return Type.CONTROL; } + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/DivNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/DivNode.java new file mode 100644 index 00000000..0db2b7a2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/DivNode.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeFloat; +import java.util.BitSet; + +public class DivNode extends ArithNode { + @Override boolean allowFloat() { return true; } + public DivNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + public DivNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs, mode); } + + + @Override public String op() { return _mode==1 ? "//" : "/"; } + + @Override long doOp( long x, long y ) { return y==0 ? 0 : x / y; } + @Override double doOp(double x, double y ) { return x / y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + // Div of 1. + if( in(2)._type == TypeInteger.TRUE && in(1)._type.isa(compute()) ) + return in(1); + + // Div of constant + if( _mode==2 ) { + if( in(2)._type == TypeFloat.constant(1.) && in(1)._type.isa(compute()) ) + return in(1); + if( in(2)._type instanceof TypeFloat f && f.isConstant() ) + return new MulNode(in(1),ConstantNode.make(TypeFloat.constant(1.0/f.value())).peephole(),_mode); + } + + return super.idealize(); + } + + @Override Node copy(Node lhs, Node rhs) { return new DivNode(lhs,rhs,_mode); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/EscapeNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/EscapeNode.java new file mode 100644 index 00000000..f7518184 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/EscapeNode.java @@ -0,0 +1,109 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * Merge public and private aliases. + * 0 - control + * 1 - private pointer + * 2 - private alias + * 3 - public alias + */ +public class EscapeNode extends TypeNode { + + public EscapeNode(Field fld, Node self, Node priv, Node pub ) { + super(fld,null,self,priv,pub); + } + public EscapeNode(EscapeNode esc ) { + super(esc); // Copy constructor + // Except we do not want/need self ptr past Opto. + // *NOTE* the weird direct stomp; this is correct because this + // constructor does NOT set the backedges, because it is used to + // copy-construct an entire graph and ALL edges will get rewritten. + _inputs.set(1,null); + _con = esc._con; + } + + // Pointer to some private (unescaped) memory from a NewNode + public Node self() { return in(1); } + public Node priv() { return in(2); } + public Node pub () { return in(3); } + public Field fld() { return (Field)_con; } + + + @Override + protected ExprPrinter _print1(ExprPrinter p) { + p.p("Esc#").p(fld()._alias).p(" {"); + if( self()==null ) p.p("---"); + else p.n(self()).p(","); + p.n(priv()).p("}, "); + p.n(pub ()); + return p; + } + + @Override + public Type compute() { + if( priv()._type.isHigh() ) return TypeMem.TOP; + if( pub ()._type.isHigh() ) return TypeMem.TOP; + TypeMem mpriv = (TypeMem)priv()._type; + TypeMem mpub = (TypeMem)pub ()._type; + // Private memory comes from an allocation or other non-aliased source. + // Public memory comes from anywhere, might be bulk memory with a very weak mem type. + Field fld = fld(); + assert mpriv._alias==1 || mpriv._alias==fld._alias; + assert mpub ._alias==1 || mpub ._alias==fld._alias; + Type tpriv = mpriv._t instanceof TypeStruct ts && ts.findAlias(fld._alias)!=-1 + ? ts.at(ts.findAlias(fld._alias))._t + : mpriv._t; + assert !(mpub._t instanceof TypeStruct); // Never expect this, but if it starts to happen, need to optimize + Type tpub = mpub._t; + + // t is meet of pub & priv. Pub memory can be very weak (e.g. alias 1) + // and we're no worse than the declared field type + Type t = tpriv.meet(tpub).join(fld._t); + int[] fidxes = XInt.meet(mpriv._escFs, mpub._escFs); + int[] aliases = XInt.meet(mpriv._escAs, mpub._escAs); + return TypeMem.make(fld._alias, t, false, false, fld._final, fidxes, aliases).escapesFrom(t); + } + + @Override public Node idealize() { + // Sharpen private memory input + if( priv() instanceof MemMergeNode mmm ) { + setDef(2,mmm.alias(fld()._alias)); + return this; + } + // Sharpen public memory input + if( pub() instanceof MemMergeNode mmm ) { + setDef(3,mmm.alias(fld()._alias)); + return this; + } + // New allocation is dead + if( self()._type.isHigh() ) + if( pub()._type.isa(_type) ) return pub(); + else addDep(pub()); + return null; + } + +// Upgrade the internal type +@Override boolean _upgradeType( HashMap TYPES) { + Type t = _con.upgradeType(TYPES); + if( t == _con ) return false; + unlock(); // Unlock before changing _con + _con = t; + return true; +} + + @Override public boolean eq(Node n) { + if( fld()._alias!=((EscapeNode)n).fld()._alias ) + return false; + assert _con == ((EscapeNode)n)._con; + return true; + } + + @Override int hash() { return _con.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternNode.java new file mode 100644 index 00000000..2d1dc29f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternNode.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; + +import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + A constant with external linkage. + */ + +public class ExternNode extends ConstantNode { + public final String _extern; + // An exact external function identity has no local FunNode/Return to own. + // Its lifetime and eventual relocation are carried by the extern symbol + // and CodeGen's FIDX-to-extern mapping instead. + public ExternNode(Type t, String ex) { super(t,true); _extern = ex; } + + + @Override public String label() { return "#"+_con+":"+_extern; } + + @Override public boolean eq(Node n) { return this==n; } + + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java new file mode 100644 index 00000000..23ed5468 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.HashMap; +import java.util.IdentityHashMap; + +// A field address supplied by the C linker, not a displacement from the base. +// The class base remains available for ideal type/alias analysis. Instruction +// selection replaces it with this symbol's address and a zero displacement. +public class ExternOffsetNode extends ExternNode { + public ExternOffsetNode(String name) { super(TypeInteger.BOT,name); } + + @Override public Node copy() { return new ExternOffsetNode(_extern); } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/FRefNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FRefNode.java new file mode 100644 index 00000000..644f1434 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FRefNode.java @@ -0,0 +1,50 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Var; +import com.seaofnodes.simple.type.Type; + +/** + * A Forward Reference. Its any final constant, including functions. When + * the Def finally appears its plugged into the forward reference, which then + * peepholes to the Def. + + * FRefs can be external references, to other Simple defined values in ELF + * files, or external FFI/C libs. If external, a file system lookup needs to + * find it in another file, and the name (one time) change to match the found + * location. + * + * Example, a ambiguous name "foo" is looked up externally; it might resolve + * to "A.B.foo" or "libc.foo" or the like; the name is changed to match. + */ +public class FRefNode extends ConstantNode { + public static final Type FREF_TYPE = Type.BOTTOM; + public String _name; + public final Parser.Lexer _loc; // Source location + + public FRefNode( String name, Parser.Lexer loc ) { this(name,loc,FREF_TYPE); } + public FRefNode( String name, Parser.Lexer loc, Type type ) { + super(type); + _name = name; + _loc = loc; + } + + @Override public String label() { return "FRef"+_name; } + + + @Override protected ExprPrinter _print1(ExprPrinter p) { + return p.p("FRef_").p(_name); + } + + @Override public Node idealize() { + // When FRef finds its definition, idealize to it + return nIns()==1 ? null : in(1); + } + + public Parser.ParseException err() { return Parser.error("Undefined name '"+_name+"'",_loc); } + + @Override public boolean eq(Node n) { return this==n; } + @Override int hash() { return _name.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/FldOffNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FldOffNode.java new file mode 100644 index 00000000..535241a6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FldOffNode.java @@ -0,0 +1,54 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; + +/** + * A field offset. This becomes a constant *after* other optimizations which + * determine field sizes - some fields might be dead, or hold only zeros or + * small values. + */ + +public class FldOffNode extends TypeNode { + public final String _fname; // Field name + public final Parser.Lexer _loc; // + public FldOffNode( Node base, String fname, Parser.Lexer loc ) { super(TypeInteger.BOT,base); _fname = fname; _loc=loc; } + + + @Override public String label() { + return _fname == " len" ? "sizeof()" : "#"+_fname; + } + + @Override protected ExprPrinter _print1(ExprPrinter p) { + return p.p(label()); + } + + @Override public Type compute() { + return Type.BOTTOM; + } + + @Override public Node idealize() { + // When base() gets typed, this xforms into a ConFldOffNode + if( in(0)._type instanceof TypeMemPtr tmp ) + return new ConFldOffNode(tmp._obj,_fname); + return null; + } + + @Override public boolean eq(Node n) { + FldOffNode off = (FldOffNode)n; // Invariant + return _fname.equals(off._fname) && super.eq(n); + } + @Override int hash() { return super.hash() * _fname.hashCode(); } + + @Override public Parser.ParseException err() { + if( in(0)._type instanceof TypeMemPtr tmp && tmp._obj._fref ) + return Parser.error("Unknown struct type '"+tmp._obj._name+"'",null); + return null; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunNode.java new file mode 100644 index 00000000..ad79130e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -0,0 +1,463 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; +import static com.seaofnodes.simple.codegen.CodeGen.CODE; + +public class FunNode extends RegionNode { + + // When set true, this Call/CallEnd/Fun/Return is being trivially inlined + public boolean _folding; + public boolean folding() { return _folding; } + + private TypeFunPtr _sig; // Initial signature + private ReturnNode _ret; // Return pointer + + public String _name; // Debug name + + public int _approxUIDs; // Approximate function size, used as a inlining heuristic + public CompUnit _compunit; // Defined in source file + public final boolean _extern; // External function; no code, name only + + private FunNode( Parser.Lexer loc, Node[] nodes, TypeFunPtr sig, String name, CompUnit compunit, boolean ext ) { + super(loc,nodes); + _name = name; + _sig = sig; + _extern = ext; + _compunit = compunit; + } + public FunNode( Parser.Lexer loc, TypeFunPtr sig, String name, CompUnit compunit, Node... nodes ) { this(loc,nodes,sig,name,compunit,false); } + public FunNode( FunNode fun ) { + super( fun, fun._loc ); + _sig = fun.sig(); + _name = fun._name; + _compunit = fun._compunit; + _extern = fun._extern; + } + + + @Override public String label() { return _name == null ? "$fun"+_sig.fidx() : _name; } + + // Find the one CFG user from Fun. It's not always the Return, but always + // the Return *is* a CFG user of Fun. + @Override public CFGNode uctrl() { + for( Node n : _outputs ) + if( n instanceof CFGNode cfg && + (cfg instanceof RegionNode || cfg.cfg0()==this) ) + return cfg; + return null; + } + + public ParmNode parm(int idx) { + ParmNode pidx = null; + for( Node n : _outputs ) + if( n instanceof ParmNode parm && parm._idx==idx ) + { assert pidx==null; pidx=parm; } + return pidx; + } + public ParmNode rpc() { return parm(0); } + + // Cannot create the Return and Fun at the same time; one has to be first. + // So setting the return requires a second step. + public void setRet(ReturnNode ret) { _ret=ret; } + public ReturnNode ret() { assert _ret!=null; return _ret; } + + // Signature can improve over time + public TypeFunPtr sig() { return _sig; } + public void setSig( TypeFunPtr sig ) { + assert sig.isa(_sig) || resolvesForwardSelf(sig,_sig); + if( _sig != sig ) { + CODE.add(this); + // Changing the signature can allow more inlining + if( _ret != null ) + for( Node use : ret().outs() ) + if( use instanceof CallEndNode || use instanceof FunPtrNode ) + CODE.add(use); + unlock(); + _sig = sig; + } + } + + // A class initializer starts parsing with an open singleton receiver and + // closes that same receiver after discovering its fields. Function + // arguments are contravariant, so ordinary isa does not describe this + // parser-time forward-reference resolution even though every other part + // of the signature sharpens normally. + private static boolean resolvesForwardSelf(TypeFunPtr sig, TypeFunPtr old) { + if( !(sig.arg(0) instanceof TypeMemPtr now) || + !(old.arg(0) instanceof TypeMemPtr prior) || + now._obj._name != prior._obj._name || + !prior._obj._open || now._obj._open ) + return false; + return sig.makeFrom(old.arg(0),0).isa(old); + } + + @Override boolean _upgradeType( HashMap TYPES) { + TypeFunPtr sig = (TypeFunPtr)_sig.upgradeType(TYPES); + if( sig == _sig ) return false; + unlock(); + _sig = sig; + return true; + } + + public void setName( String name ) { + if( _name==null ) + _name = _compunit==null || _compunit._cname==null || _compunit._cname.indexOf('.')==-1 + ? name + : (_compunit._cname+"."+name).intern(); + } + + @Override + public Type compute() { + // Only dead if no callers after SCCP + if( unknownCallers() ) + return Type.CONTROL; + Type t = Type.XCONTROL; + for( int i = 1; i < nIns(); i++) { + // Since there are no unknown callers, the Start input is only a + // fake hook and will be a Tuple with XControl, so ignore it. Need + // to be called from someplace other than Start. + if( !(in(i) instanceof StartNode) ) + t = t.meet(in(i)._type); + } + return t; + } + + @Override + public Node idealize() { + + // Some linked path dies, except unknown caller hooks never kill Start. + Node progress = deadPath(unknownCallers()); + if( progress!=null ) { + if( nIns()==2 && in(1) instanceof CallNode call ) + CODE.add(call.cend()); // If no Start and one call, check for inline + return progress; + } + + // Upgrade inferred or user-written return type to actual + if( _ret!=null && _ret._type instanceof TypeTuple tt && + tt.ret() != _sig.ret() && tt.ret().isa(_sig.ret()) ) { + setSig(_sig.makeFrom(tt.ret())); + return this; + } + + // Attempt to get rid of the unknown caller. + // - Must be past pessimistic Iter, which discovers and links calls + // through class-field loads and other initially imprecise values. + // - FIDX is not in the escape set + // - Not called anywhere else; post Opto called-not-escaped functions need to still be hooked to start + // Opaque CLInit obligations need their bodies even without Call edges. + if( !isClz() && in(1) instanceof StartCUNode start ) { + boolean preOpto = CodeGen.CODE._phase == null || + CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal(); + if( !preOpto && !start.escapedFIDX(_sig.fidx()) && nIns()<=2 && _ret!=null ) { + _compunit._stop.delDef(_compunit._stop._inputs.find(ret())); + return removeDeadPath(1); + } else { + addDep(start); + } + } + + + // If no default/unknown caller, use the normal RegionNode ideal rules + // to collapse + if( unknownCallers() ) return null; + + // If folding (already inlined) down to a single input, become that input. + // Cannot collapse if not-inlining, and might not inline if call site + // calls multiple targets. + if( _folding && nIns()==2 && !hasPhi() ) { + CODE.add( CODE._stop ); // Stop will remove dead path + CODE.add( _ret ); // Return will compute to TOP control + return in(1); // Collapse if no Phis; 1-input Phis will collapse on their own + } + + return null; + } + + // Bypass Region idom, always assume depth == 1, one more than Start, + // unless folding + @Override public int idepth() { return _folding ? super.idepth() : 1; } + + // Bypass Region idom, always assume idom is Start + @Override public CFGNode idom(Node dep) { + return _folding ? cfg(1) : null; + } + + // Always in-progress until we run out of unknown callers + public boolean unknownCallers() { return nIns()>=2 && in(1) instanceof StartNode; } + + public boolean isModInit( ) { + // The one top-level with no internal dots: + // "sys." is a module, but "sys.io." is not. + return isClz() && _name.indexOf('.')==_name.lastIndexOf('.'); + } + public boolean isClz ( ) { return isClz (_name); } + public boolean isInit( ) { return isInit(_name); } + public boolean isInstance() { return isInstance(_name); } + public static boolean isClz (String name ) { return name!=null && name.endsWith("."); } + public static boolean isInit(String name ) { return name!=null && name.endsWith("init>"); } + public static boolean isInstance(String name ) { return name!=null && name.endsWith("."); } + + // Allocation factories and instance initializers carry extra memory boilerplate. + public int inlineLimit() { + return isInstance() || (_name!=null && _name.endsWith(".")) ? 200 : 100; + } + + // Function is public (callable from Start directly). + public boolean isPublic( ) { + // Never true for anonymous functions + if( _name == null ) return false; + // Only public for and , all others + // are found via field loads. The Start hook also supplies its + // conservative pre-Opto construction inputs; StopCU separately keeps + // its private-memory return from escaping as a public result. + if( !isInit() ) return false; + // Private class, has to be found via program text, not linker + if( _name.charAt(0)=='_' ) return false; + + // No instances of "...clz1._clz2..." where the class name path + // includes a private class. + if( _name.indexOf("._") != -1 ) return false; + + // Public class + return true; + } + + @Override public boolean inProgress() { + return unknownCallers() && CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal(); + } + + // Build the function body set + public BitSet body() { + // Iter drains ordinary peeps between inlines; body discovery precedes + // this inline's folding, so function boundaries are stable. + assert !_folding; + // Forward control includes paths that never reach Return. + BitSet cfgs = new BitSet(); + walkCFG(this,cfgs); + cfgs.set(ret()._nid); + + // Top down (start to stop) all flavors. CFG limit to bitmap. + // If data use bottoms out in wrong CFG, returns false - but tries all outputs. + // If any output hits an in-CFG use (e.g. phi), then keep node. + BitSet body = new BitSet(); + // The function owns its Return even when it has no executable exit. + body.set(ret()._nid); + BitSet visit = new BitSet(); + int old; + do { + old = body.cardinality(); + visit.clear(); + walkDown(this, cfgs, body, visit); + } while( old != body.cardinality() ); + return body; + } + + private static void walkCFG(CFGNode n, BitSet cfgs) { + if( cfgs.get(n._nid) ) return; + cfgs.set(n._nid); + if( n instanceof ReturnNode ) return; + for( Node use : n._outputs ) { + if( !(use instanceof CFGNode cfg) || cfg instanceof FunNode ) continue; + if( cfg instanceof RegionNode ? cfg._inputs.find(n)>0 : cfg.in(0)==n ) + walkCFG(cfg,cfgs); + } + } + + private static boolean walkDown( Node n, BitSet cfgs, BitSet body, BitSet visit ) { + if( n==null ) return false; + if( visit.get(n._nid) ) return body.get(n._nid); + visit.set(n._nid); + // Visit self as CFG outside the function + if( n instanceof CFGNode && !cfgs.get(n._nid) ) + return false; + // Pretend the NewNode is a CFG, so its projections stay with it in loops + if( n instanceof NewNode nnn ) cfgs.set(n._nid); + // Visit n.cfg() outside of function + if( n.in(0)!=null && !cfgs.get(n.in(0)._nid) ) + return false; + // Phis inside the function must have their body flag set BEFORE + // recursion walks around a loop and finds them again. + if( n instanceof PhiNode phi && cfgs.get(phi.in(0)._nid) ) + body.set(phi._nid); // Find data cycles + boolean in = n.in(0)!=null || n instanceof CFGNode; + for( Node use : n._outputs ) { + // Will hit a backedge, so 'n' MUST be in the function, and must be + // set before the cyclic walk finds 'n' again. + if( use instanceof PhiNode puse && puse.in(0) instanceof LoopNode loop && puse.in(2)==n ) + body.set(n._nid); // Set before walk + in |= walkDown(use,cfgs,body,visit); + } + if( in ) body.set(n._nid); + return in; + } + + // Clone function body. Give function a new FIDX. + FunNode copyBody() { + // Build the function body BitSet + BitSet body = body(); + assert body.cardinality() < inlineLimit(); + // Walk the body, cloning + IdentityHashMap map = new IdentityHashMap<>(); + BitSet visit = CodeGen.CODE.visit(); + bodyCopy( visit, body, map, this ); + bodyCopy( visit, body, map, _ret ); // May be disconnected from executable control. + visit.clear(); + bodyEdge( visit, body, map, this ); + bodyEdge( visit, body, map, _ret ); + visit.clear(); + assert map.size()==body.cardinality(); + + // New function/return cross-link each other + FunNode fun2 = (FunNode )map.get(this); + ReturnNode ret2 = (ReturnNode)map.get(_ret); + fun2._ret = ret2; + ret2._fun = fun2; + BitSet body2 = new BitSet(); + for( Node n : map.values() ) + body2.set(n._nid); + + // Remove non-body callers. Body-local call edges were cloned along + // with their CallEnds and must stay linked so their cloned types remain + // monotonic during the post-Opto Iter. + for( int i=1; i Integer.compare(x._nid,y._nid)); + for( Node old : olds ) + if( old instanceof CallNode oldCall ) + relinkClonedCall(oldCall,(CallNode)map.get(oldCall),map); + + // Flip to a new FIDX to avoid confusion with the old one. + // This is a non-monotonic (sideways) type move, only applicable + // because fun2 is new. The fidx is also a purely local fiction, + // since this body will inline and the fidx goes dead. + fun2._sig = _sig.makeFrom(CodeGen.CODE._fidxs.nextInline()); + return fun2; + } + + private static void relinkClonedCall( CallNode oldCall, CallNode call, IdentityHashMap map ) { + CallEndNode oldCend = oldCall.cend(); + if( oldCend==null || oldCend._folding ) + return; + for( int i=1; i 1 ) + cend._type = cend.compute(); + } + + // Clone small function + private void bodyCopy( BitSet visit, BitSet body, IdentityHashMap map, Node n ) { + if( n==null ) return; + if( !body.get(n._nid) ) return; // Not part of the body + if( visit.get(n._nid) ) return; // Been there, done that + visit.set(n._nid); + Node m = n.copy(); + CodeGen.CODE.add(m); + map.put(n,m); + m._type = n._type; + for( Node x : n._outputs ) + bodyCopy(visit,body,map,x); + } + private void bodyEdge( BitSet visit, BitSet body, IdentityHashMap map, Node n ) { + if( n==null ) return; + if( !body.get(n._nid) ) return; // Not part of the body + if( visit.get(n._nid) ) return; // Been there, done that + visit.set(n._nid); + Node m = map.get(n); + for( Node e : n._inputs ) { + Node x = e; + if( e != null ) { + if( n instanceof CallEndNode cend && e != cend.call() && + (!(e instanceof ReturnNode ret) || map.get(ret.fun()) == null) ) + continue; + x = map.get(e); + if( x == null ) { + if( n instanceof CallEndNode cend && e != cend.call() ) + continue; + x = e; + } + } + m.addDef(x); + } + for( Node x : n._outputs ) + bodyEdge(visit,body,map,x); + } + + + // no 2 FunNodes are ever GVN'able + @Override public boolean eq( Node n ) { + return this==n; + } + + @Override public int hash() { + return _sig.hashCode(); + } + + // ------------ + // MachNode specifics, shared across all CPUs + + // Function may call other functions? X86 requires 16b aligned SP on + // outbound calls, but not leaf. Set during an unrelated RA scan. + public boolean _hasCalls; + + // Maximum incoming argument slot, based on function type + public int _maxArgSlot; + + // Frame adjust, including padding, set during early Encoding + public int _frameAdjust; + + public String op() { return _frameAdjust==0 ? "entry" : "subi"; } + public void postSelect(CodeGen code) { + code.link(this); + // Max slot seen past space reserved for incoming arguments + _maxArgSlot = code._mach.maxArgSlot(sig()); + } + public RegMask regmap(int i) { return null; } + public RegMask outregmap() { return null; } + public void encoding( Encoding enc ) { throw Utils.TODO(); } + public void asm(CodeGen code, SB sb) { + if( _frameAdjust!=0 ) + sb.p("rsp -= #").p(_frameAdjust); + } + + public void computeFrameAdjust(CodeGen code, int maxReg) { + // Max slot seen, or 0. + int maxSlot = Math.max(maxReg - code._mach.regs().length,_maxArgSlot); + // Frame adjust before rounding, can be negative for no spills + int size = maxSlot - _maxArgSlot; + _frameAdjust = size*8; + } + + // Given a high register number and during encoding compute the stack + // pointer offset. Pre-RA we lack the fun._frameAdjust. We also need a + // FunNode. + public int computeStackOffset(CodeGen code, int reg) { + String[] regs = code._mach.regs(); + int stackSlot = reg - regs.length; + int slotRotate = stackSlot < _maxArgSlot + ? stackSlot + (_frameAdjust>>3) + : stackSlot - _maxArgSlot; + return slotRotate*8; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java new file mode 100644 index 00000000..3d6031b8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Constant function pointer whose type follows the callee signature. +public class FunPtrNode extends TypeNode { + public FunPtrNode(TypeFunPtr tfp, StartNode start, ReturnNode ret) { + super(tfp,start,ret); + } + public FunPtrNode(FunPtrNode fptr) { super(fptr); } + + + public ReturnNode ret() { return (ReturnNode)in(1); } + public FunNode fun() { return ret().fun(); } + + @Override public String label() { return "#"+_con; } + @Override public Node copy() { + FunPtrNode fptr = new FunPtrNode((TypeFunPtr)_con,null,null); + fptr._type = _type; + return fptr; + } + + @Override + protected ExprPrinter _print1(ExprPrinter p) { + FunNode fun = CodeGen.CODE._link(((TypeFunPtr)_con).fidx()); + return fun!=null && fun._name!=null + ? p.p("{ ").p(fun._name).p("}") + : p.p(_con); + } + + @Override public boolean isConst() { return true; } + @Override public Type compute() { + ReturnNode ret = ret(); + if( ret == null ) return _con; + if( !(ret._type instanceof TypeTuple tt) ) return ret._type.oob(); + TypeFunPtr tfp = ret.fun().sig(); + // Both the sig and the return value must be true, so JOIN both + Type tret = tfp._ret.join(tt.ret()); + return tfp.makeFrom(tret); + } + @Override public Node idealize() { + ReturnNode ret = ret(); + if( ret==null ) return null; + // Function died (never executed), but fcn ptr is alive. + // Can be checked for null, or for unequals another FunPtr. + FunNode fun = addDep(ret.fun()); + TypeFunPtr sig = fun.sig(); + if( sig != _con ) + { liftType(sig); return this; } + // TODO: If the function body dies but this value remains live only as + // a null/equality sentinel, preserve the identity without the Return. + if( fun.isDead() ) + { setDef(1,null); liftType(sig.makeFrom(Type.TOP)); return this; } + return null; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardMach.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardMach.java new file mode 100644 index 00000000..202b1ab8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardMach.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; + +public class GuardMach extends GuardNode implements MachNode { + public GuardMach(GuardNode guard) { super(guard); } + @Override public String op() { return label(); } + @Override public RegMask regmap(int i) { assert i==1; return RegMask.FULL; } + @Override public RegMask outregmap() { return RegMask.FULL; } + @Override public int twoAddress() { return in(1)._type==Type.BOTTOM ? 0 : 1; } + @Override public void encoding(Encoding enc) { } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(in(1))); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardNode.java new file mode 100644 index 00000000..e985da72 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardNode.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeScalar; +import com.seaofnodes.simple.util.Utils; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// A branch-proven refinement to zero/null or non-zero/non-null. The value +// family is deliberately taken from the input when it becomes known. +public class GuardNode extends Node { + public final boolean _nonZero; + + public GuardNode(boolean nonZero, Node ctrl, Node val) { + super(ctrl,val); + _nonZero = nonZero; + } + + public GuardNode(GuardNode guard) { + super(guard); + _nonZero = guard._nonZero; + } + + + @Override public String label() { return _nonZero ? "(!=0)" : "(==0)"; } + + @Override protected ExprPrinter _print1(ExprPrinter p) { + return p.p(label()).n(in(1)); + } + + @Override public Type compute() { + // Guard is a pinned, path-local fact. If its control path is not + // executable, the fact is not executable either, regardless of how the + // value input sharpens later. + Type c = in(0)._type; + if( c != Type.CONTROL && c != Type.BOTTOM ) + return Type.TOP; + Type t = in(1)._type; + if( t==Type.BOTTOM || t==Type.TOP ) return t; + // The scalar envelope does not identify a value family. Preserve it + // until SCCP learns whether this is an int, float, pointer, or fptr; + // choosing generic ptr here can be weaker than a later concrete type. + if( t==TypeScalar.BOT || t==TypeScalar.TOP ) return t; + Type t2 = _nonZero ? t.nonZero() : t.makeZero(); + return t2==null ? (_type==null ? Type.TOP : _type) : t2; // No sane answer for e.g. not-zero of a zero + } + + @Override public Node idealize() { + if( in(0)._type != Type.CONTROL && in(0)._type != Type.BOTTOM ) + return null; + if( in(1)._type==Type.BOTTOM || in(1)._type==Type.TOP ) return null; + if( _type.isHighOrConst() && in(1)._type.isa(_type) ) + return in(1); + return null; + } + + @Override public boolean eq(Node n) { return _nonZero==((GuardNode)n)._nonZero; } + @Override int hash() { return _nonZero ? 1 : 0; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/IfNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/IfNode.java new file mode 100644 index 00000000..471a28fe --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/IfNode.java @@ -0,0 +1,100 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class IfNode extends CFGNode implements MultiNode { + + public IfNode(Node ctrl, Node pred) { + super(ctrl, pred); + CodeGen.CODE.add(this); // Because idoms are complex, just add it + } + public IfNode(IfNode iff) { super(iff); } + + + @Override + protected ExprPrinter _print1(ExprPrinter p) { + p.p("if( "); + if( in(1)==null ) p.p("never"); + else p.n(in(1)); + return p.p(" )"); + } + + public Node ctrl() { return in(0); } + public Node pred() { return in(1); } + + // No one unique control follows + @Override public CFGNode uctrl() { return null; } + + @Override + public Type compute() { + // If the If node is not reachable then neither is any following Proj + assert !_inputs.isEmpty(); + 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.isHigh() ) + return TypeTuple.IF_NEITHER; + // If constant is 0 then false branch is reachable + // Else true branch is reachable + if( t.isConstant() ) + return (t==Type.NIL || t==TypeInteger.ZERO || (t instanceof TypeFunPtr tfp && tfp._fidxs== XInt.EMPTY) ) ? TypeTuple.IF_FALSE : TypeTuple.IF_TRUE; + // If adding a zero makes a difference, the predicate must not have a zero/null + if( !t.makeZero().isa(t) ) + return 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( CFGNode dom = idom(), prior=this; dom!=null; prior = dom, dom = dom.idom() ) + if( addDep(dom) instanceof IfNode iff && + prior instanceof CProjNode prj && + (addDep(iff.pred())==pred() || + pred() instanceof GuardNode cast && cast._nonZero && iff.pred()==cast.in(1) ) ) { + setDef(1,con( prj._idx==0 ? 1 : 0 )); + return this; + } + return null; + } + + // MachNode variants need to support this and negate the conditional test. + // The following CProjs will be inverted by the caller. + public void negate() { throw Utils.TODO(); } + + // Negate the sense of a test + public static String negate( String bop ) { + return switch( bop ) { + case "<" -> ">="; + case "<=" -> ">" ; + case "==" -> "!="; + case "!=" -> "=="; + case ">" -> "<="; + case ">=" -> "<" ; + default -> throw Utils.TODO(); + }; + } + + // Swap compare operands + public static String swap( String bop ) { + return switch( bop ) { + case "<" -> ">" ; + case "<=" -> ">="; + case "==" -> "=="; + case "!=" -> "!="; + case ">" -> "<" ; + case ">=" -> "<="; + default -> throw Utils.TODO(); + }; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoadNode.java new file mode 100644 index 00000000..e1a495ad --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -0,0 +1,331 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +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 { + + /** + * Load a value from a ptr.field. + * + * @param name The field we are loading + * @param mem The memory alias node - this is updated after a Store + * @param ptr The ptr to the struct base from where we load a field + * @param off The offset inside the struct base + */ + public LoadNode(Parser.Lexer loc, String name, int alias, Node ctrl, Node mem, Node ptr, Node off) { + super(loc, name, alias, true, Type.BOTTOM, ctrl, mem, ptr, off); + } + + + @Override boolean canDrop(MemOpNode other, Node dep) { + return super.canDrop(other,dep) && !clobbered(dep); + } + + // Check only immediate memory users. Writers clobber memory; Phis and + // aggregates might lead to a clobber, so stop rather than search further. + private boolean clobbered(Node dep) { + Node mem = mem(); + dep.addDepForwards(mem); + for( Node use : mem._outputs ) { + if( use==null ) continue; + dep.addDepForwards(use); + if( use instanceof StoreNode || use instanceof PhiNode || + use instanceof MemMergeNode || + use instanceof NewNode || use instanceof EscapeNode || use instanceof CallNode || + use instanceof CLInitNode ) return true; + } + return false; + } + + // Debugger label + @Override public String label() { return "ld_"+mlabel(); } + @Override + protected ExprPrinter _print1(ExprPrinter p) { return p.p(".").p(_name); } + + @Override + public Type compute() { + Type mem0 = mem()._type; + Type ptr0 = ptr()._type; + // Validate argument types + if( ptr0.isHigh() ) + return TypeScalar.TOP; + if( !(mem0 instanceof TypeMem mem) ) + return mem0.isHigh() ? TypeScalar.TOP : TypeScalar.BOT; + if( mem._t == Type.TOP ) + return TypeScalar.TOP; + if( ptr0 == Type.NIL ) + return Type.BOTTOM; // Load from nil is error + if( ptr0 == Type.BOTTOM ) + return Type.BOTTOM; // Error in is error out + if( !(ptr0 instanceof TypeMemPtr tmp) ) + return scalar(mem._t); + if( tmp.nullable() ) + return Type.BOTTOM; // Load from nil-able is error + + // Load field from object + Field pfld = tmp._obj.field(_name); + // No field? Open objects might yet get the field when falling; + // closed objects with missing field are an error. + if( pfld == null ) + return tmp.isHigh() || tmp._obj.isHigh() ? TypeScalar.TOP : TypeScalar.BOT; + + Type t = pfld._t; + boolean readonly = t instanceof TypeMemPtr view && view._ro; + // Load member of constant array + if( t instanceof TypeConAry ary ) + t = ary.elem(); // TODO: if offset is known, can peek the constant + if( mem._alias==_alias ) + t = t.join(mem._t); + + // A deeply read-only base produces a deeply read-only value. The + // generic declared field type must not cast this information away. + if( ptr0.isFinal() || readonly ) + t = t.makeRO(); + return scalar(t); + } + + // Loads produce ordinary scalar values. Field/memory joins can still + // expose the enclosing global lattice bounds through an uninitialized + // Type.TOP/BOTTOM field; keep those results inside the scalar envelope. + private static Type scalar(Type t) { + return t==Type.TOP ? TypeScalar.TOP : t==Type.BOTTOM ? TypeScalar.BOT : t; + } + + @Override + public Node idealize() { + Node mem = mem(); + Node ptr = ptr(); + + // Null checks are handled by types. Array elements retain control for + // range checks, but reading an array's length has no index to check. + if( in(0)!=null && ptr._type instanceof TypeMemPtr tmp && + (!tmp._obj.isAry() || _name.equals("#")) ) { + setDef(0,null); + return this; + } + + // Forward-ref loads eventually sharpen to a declared type + Field fld; + if( ptr._type instanceof TypeMemPtr tmp && + (fld=tmp._obj.field(_name)) != null && + _alias != fld._alias) { + assert _alias==1 || _alias == fld._alias; + assert !tmp._obj._open && !tmp._obj._fref; + unlock(); // Alias participates in GVN semantics + _alias = fld._alias; + return this; + } + // Must sharpen alias first + if( _alias == 1 ) + return null; + + Node slice = CLInitNode.slice(mem,_alias,this); + if( slice!=mem ) { setDef(1,slice); return this; } + + if( mem instanceof MemMergeNode merge ) { + setDef(1,merge.alias(_alias)); + // In support of uplifting control to a prior dominating load, if + // we move our memory, recheck other loads to see if they can hoist. + for( Node use : in(1).outs() ) + if( use instanceof LoadNode ld && use != this && ptr == ld.ptr() ) + CodeGen.CODE.add(ld); + + return this; + } + + // Uplift control to a prior dominating load. + for( Node memuse : mem._outputs ) + // Find a prior load, has same mem,ptr,off but higher ctrl + if( memuse != this && memuse instanceof LoadNode ld && ptr==ld.ptr() && off()==ld.off() && + cfg0()!=null && cfg0().domLCA(ld.cfg0(),this) == ld.cfg0() && // Higher control means load is legal earlier + addDep(ld)._type.isa(_type) ) // and not rolling backwards + return ld; + + Node win = find(mem,ptr,null,null); + if( win!=null ) return folded(win); + if( ptr instanceof ReadOnlyNode ro ) ptr=ro.in(1); + + // 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 && + // Retain control on both copies only if it dominates both paths. + // The Region itself, or a later range check, cannot move to its inputs. + (in(0)==null || cfg0().domLCA(memphi.region().idom(this),this)==cfg0()) && + // Offset can be hoisted + off() instanceof ConstantNode && + // Pointer can be hoisted + hoistPtr(ptr,memphi) ) { + + // Returning to this memory means no change only if the pointer + // is also loop invariant. A pointer Phi may select another object. + Node stop = memphi.region() instanceof LoopNode && + !(ptr instanceof PhiNode p && p.region()==memphi.region()) ? memphi : null; + if( find(memphi.in(2),ptr,stop,new BitSet())!=null || + (!(memphi.region() instanceof LoopNode) && find(memphi.in(1),ptr,null,new BitSet())!=null) ) { + // Integer loop Phis widen to i64. Keep the load's declared + // width at the result, as for ordinary typed loop variables. + if( _type instanceof TypeInteger && !declaredType().isa(_type) ) return null; + PhiNode phi = new PhiNode(_name,memphi.region()).keep(); + phi.setType(_type instanceof TypeInteger ? TypeInteger.BOT : _type); + for( int i=1; i _print1(ExprPrinter p) { + p.open().p(op()).p(","); + for( int i=1; i _print1(ExprPrinter p) { + p.p("MEM[ "); + for( int j=2; j=nIns() || in(i)==null ) + setDefX(i,mem.in(i)); + } + } + CodeGen.CODE.add(mem.in(1)); + setDef(1,mem.in(1)); + return unkeep(); + } + + return progress ? this : null; + } + + + public Node in( Var v ) { return in(v._idx); } + + public Node alias( int alias ) { + assert !(in(1) instanceof BulkMemPhiNode bulk && bulk.isSplit(alias) && + (alias >= nIns() || in(alias)==null)); + return alias < nIns() && in(alias)!=null ? in(alias) : in(1); + } + + public Node alias( int alias, Node st ) { return setDefX(alias,st); } + + // Now one-time do a useless-phi removal + void _useless( ) { + for( int i=1; i _print1(ExprPrinter p) { return _printMach(p); } + protected ExprPrinter _printMach(ExprPrinter p) { throw Utils.TODO(); } + + + // Extra conditions for factoring matching memory operations through a Phi. + // The caller has already checked the opcode, input shape and control. + boolean canDrop(MemOpNode other, Node dep) { + dep.addDep(ptr()); + return _alias>1 && _alias==other._alias && declaredType()==other.declaredType() && err()==null; + } + + @Override + public boolean eq(Node n) { + MemOpNode mem = (MemOpNode)n; // Invariant + return _alias==mem._alias && super.eq(mem); + } + + @Override + int hash() { return _alias ^ super.hash(); } + + @Override + public Parser.ParseException err() { + Type ptr = ptr()._type; + // Already an error, but better error messages come from elsewhere + if( ptr == Type.BOTTOM ) return null; + if( ptr.isHigh() ) return null; // Assume it will fall to not-null + // Better be a not-nil TMP + if( !(ptr instanceof TypeMemPtr tmp && tmp.notNull()) ) + return Parser.error( "Might be null accessing '" + _name + "'",_loc); + if( tmp._obj.field(_name)==null && + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.FinalizeIR.ordinal() ) + return Parser.error("Accessing unknown field '"+_name+"' from '*"+tmp._obj._name+"'",_loc); + // Sane field + if( off() instanceof ConFldOffNode && CodeGen.CODE._phase.ordinal() > CodeGen.Phase.FinalizeIR.ordinal() ) + return Parser.error("Accessing unknown field '"+_name+"' from '*"+tmp._obj._name+"'",_loc); + return null; + } + + // Memory writers which must follow this read. A packaging node is not a + // write; follow this alias through aggregates to its writers. + public Ary antiDeps( Ary antideps ) { + antideps.clear(); + return antiDeps(mem(),antideps); + } + + private Ary antiDeps( Node mem, Ary antideps ) { + for( Node use : mem._outputs ) { + if( use instanceof MemMergeNode merge ) { + if( merge.alias(_alias)==mem ) + antiDeps(merge,antideps); + } else if( use instanceof CallNode || use instanceof CLInitNode || + (use instanceof MemOpNode st && !st._isLoad && st._alias==_alias) || + (use instanceof EscapeNode esc && esc.fld()._alias==_alias) || + (use instanceof MemPhiNode phi && phi._alias==_alias) || + (use instanceof BulkMemPhiNode phi && !phi.isSplit(_alias)) ) { + if( !antideps.contains(use) ) antideps.add(use); + } + } + return antideps; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java new file mode 100644 index 00000000..5ae716a7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** A Phi for one precise memory alias. */ +public class MemPhiNode extends PhiNode { + + public final int _alias; + + public MemPhiNode(String label, int alias, Node... inputs) { + super(label, inputs); + assert alias > 1; + _alias = alias; + } + + public MemPhiNode(MemPhiNode phi) { + super(phi,phi._label); + _alias = phi._alias; + } + + + @Override + public Type compute() { + Type t = super.compute(); + assert BulkMemPhiNode.checkMem(this,_alias); + if( !(t instanceof TypeMem tmem) ) return t; + assert tmem._alias==1 || tmem._alias==_alias; + // This Phi structurally knows its alias, but not whether the field is + // final. Do not infer slice finality from evolving input types. + return TypeMem.make(_alias,tmem._t,tmem._one,tmem._clz,false,tmem._escFs,tmem._escAs); + } + + @Override + public Node idealize() { + // A precise memory Phi can always bypass a MemMerge and select its + // matching slice. + for( int i=1; i outs(); + + // Find a projection by index + default ProjNode proj( int idx ) { + for( Node out : outs() ) + if( out instanceof ProjNode prj && prj._idx==idx ) + return prj; + return null; + } + + // Find a projection by index + default CProjNode cproj( int idx ) { + for( Node out : outs() ) + if( out instanceof CProjNode prj && prj._idx==idx ) + return prj; + return null; + } + + // Return not-null if this projection index is a ideal copy. + // Called by ProjNode ideal and used to collapse Multis. + default Node pcopy(int idx, Node dep) { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/NeverNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NeverNode.java new file mode 100644 index 00000000..a7b4cf65 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NeverNode.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeTuple; + + +// "Never true" for infinite loop exits +public class NeverNode extends IfNode { + public NeverNode(NeverNode ctrl) { super(ctrl); } + public NeverNode(float ignore, Node ctrl) { super(ctrl,null); } + + + @Override public String label() { return "Never"; } + + @Override protected ExprPrinter _print1(ExprPrinter p) { return p.p("Never"); } + + @Override public Type compute() { return TypeTuple.IF_BOTH; } + + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/NewNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NewNode.java new file mode 100644 index 00000000..5cbe1b61 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NewNode.java @@ -0,0 +1,98 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * Allocation! Allocate a chunk of memory, no init. + * Takes in ctrl and size, and TypeStruct. + * Produces a ptr and private memory. +*/ +public class NewNode extends Node implements MultiNode { + public TypeStruct _ts; + public NewNode( TypeStruct ts, Node ctrl, Node size ) { super(ctrl,size); _ts = ts; } + + public Node ctrl() { return in(0); } + public Node size() { return in(1); } + + // --- Serialization + public NewNode(NewNode nnn) { super(nnn); _ts = nnn._ts; } + + + @Override public String label() { return "new_"+(_ts.isAry() ? "ary_"+_ts._fields[1]._t.str() : _ts.str()); } + + @Override protected ExprPrinter _print1(ExprPrinter p) { + return p.p("new ").p(_ts.str()); + } + + @Override public TypeTuple compute() { + if( ctrl()._type.isHigh() ) + return TypeTuple.DEAD_NEW; + TypeStruct ts = _ts.isAry() ? _ts : _ts.makeInit(); + return TypeTuple.make(TypeMemPtr.make(_ts), + // Arrays get pre-zeroed by constructor. + // Objects start with default-zero memory for + // fields that can be zero-initialized. This is + // still private memory, so no aliases escape yet. + TypeMem.make(1,ts,true,false,false,null,null)); + } + + @Override public Node idealize() { return null; } + + @Override boolean _upgradeType( HashMap TYPES) { + Type old = _ts; _ts = (TypeStruct)_ts.upgradeType(TYPES); return old != _ts; + } + + // Two NewNodes are never equal; NewNodes always make independent pointers + // so they are not functional - they internally bump-ptr allocate always a + // new pointer. + @Override public boolean eq(Node n) { return this==n; } + + @Override int hash() { return _ts.hashCode(); } + + + @Override public Parser.ParseException err() { + return _ts._fref ? Parser.error("Unknown struct type '"+_ts._name+"'",null) : null; + } + + + // ------------ + // MachNode specifics, shared across all CPUs + public int _arg2Reg, _xslot; + private RegMask _arg3Mask; + private RegMask _retMask; + private RegMask _kills; + public void cacheRegs(CodeGen code) { + _arg2Reg = code._mach.callArgMask(TypeFunPtr.CALLOC,2,0,code._cCallingConv).firstReg(); + _arg3Mask = code._mach.callArgMask(TypeFunPtr.CALLOC,3,0,code._cCallingConv); + // Return mask depends on TFP (either GPR or FPR) + _retMask = code._mach.retMask(TypeFunPtr.CALLOC); + // Kill mask is all caller-saves, and any mirror stack slots for args + // in registers. + RegMaskRW kills = code._cCallerSave.copy(); + // Start of stack slots + int maxReg = code._mach.regs().length; + // Incoming function arg slots, all low numbered in the RA + int fslot = cfg0().fun()._maxArgSlot; + // Killed slots for this calls outgoing args + int xslot = code._mach.maxArgSlot(TypeFunPtr.CALLOC,code._cCallingConv); + _xslot = (maxReg+fslot)+xslot; + for( int i=0; i implements Cloneable { + + /** + * 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 CFGNode} node. + */ + public Ary _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 Ary _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; + + Node(Node... inputs) { + super(CODE.getUID()); // allocate unique dense ID + _inputs = new Ary<>(Node.class); + Collections.addAll(_inputs,inputs); + _outputs = new Ary<>(Node.class); + for( Node n : _inputs ) + if( n != null ) + n.addUse( this ); + } + + // Make a Node using the existing arrays of nodes. + // Used by any pass rewriting all Node classes but not the edges. + Node( Node n ) { + super(CODE.getUID()); // allocate unique dense ID + assert CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Select.ordinal(); + _inputs = new Ary<>(n==null ? new Node[0] : n._inputs.asAry()); + _outputs = new Ary<>(Node.class); + _type = n==null ? Type.BOTTOM : n._type; + _deps = null; + _hash = 0; + } + + // Easy reading label for debugger, e.g. "Add" or "Region" or "EQ" + public String label() { return getClass().getSimpleName().replace("Node",""); } + + public String p(int depth) { return IRPrinter.prettyPrint(this,depth); } + + public boolean isConst() { return false; } + + // ------------------------------------------------------------------------ + // Graph Node & Edge manipulation + + @Override public String typeName() { return _type==null ? null : _type.toString(); } + + /** + * 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 final Ary outs() { return _outputs; } + + public int nIns() { return _inputs.size(); } + + public int nOuts() { return _outputs.size(); } + + public boolean isUnused() { return nOuts() == 0; } + + public CFGNode cfg0() { return (CFGNode)in(0); } + + /** + * 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 N setDef(int idx, N 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); + // Set the new_def over the old (killed) edge + _inputs.set(idx,new_def); + if( old_def != null ) { // If the old def exists, remove a def->use edge + if( old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + else CODE.add(old_def); // Else old lost a use, so onto worklist + } + moveDepsToWorklist(); + // Return new_def for easy flow-coding + return new_def; + } + public N setDefX(int idx, N new_def ) { + while( nIns() <= idx ) addDef(null); + return setDef(idx,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. + public Node delDef(int idx) { + unlock(); + Node old_def = in(idx); + _inputs.del(idx); + if( 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(); + return this; + } + + // Insert the numbered input, sliding other inputs to the right + public void insertDef(int idx, Node new_def) { + _inputs.insert(null,idx); + setDef(idx,new_def); + } + + /** + * 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 + */ + public 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 + @SuppressWarnings("unchecked") + protected N addUse(Node n) { _outputs.add(n); return (N)this; } + + // 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. + public boolean delUse( Node use ) { + _outputs.del(_outputs.find(use)); + moveDepsToWorklist(); // User-count and anti-dependence queries can now change. + return _outputs.isEmpty(); + } + + // Shortcut for "popping" until n nodes. A "pop" is basically a + // setDef(last,null) followed by lowering the nIns() count. + public void popUntil(int n) { + unlock(); + while( nIns() > n ) { + Node old_def = _inputs.pop(); + if( old_def != null && // If it exists and + old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + } + } + public Node removeLast() { + Node old_def = _inputs.pop(); + if( old_def != null ) + old_def.delUse(this); + return old_def; + } + + /** + * Kill a Node with no uses, by setting all of its defs + * to null. This may recursively kill more Nodes, and is basically dead + * code elimination. + */ + public void kill( ) { + assert isUnused(); // Has no uses, so it is dead + unlock(); + moveDepsToWorklist(); + _type=null; // Flag as dead + while( nIns()>0 ) { // Set all inputs to null, recursively killing unused Nodes + Node old_def = _inputs.removeLast(); + // Revisit neighbor because removed use + if( old_def != null ) { + if( CODE.add(old_def).delUse(this) ) + old_def.kill(); // If we removed the last use, the old def is now dead + old_def.moveDepsToWorklist(); // Use-count changes can enable distant rewrites. + } + } + assert isDead(); // Really dead now + } + + // Preserve CFG use-ordering when killing + public void killOrdered() { + CFGNode cfg = cfg0(); + cfg._outputs.remove(cfg._outputs.find(this)); + _inputs.set(0,null); + kill(); + } + + // 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() { return addUse(null); } + // Remove bogus null. + @SuppressWarnings("unchecked") + public N unkeep() { + delUse(null); + return (N)this; + } + // Test "keep" status + public boolean iskeep() { return _outputs.find(null) != -1; } + public void unkill() { + if( unkeep().isUnused() ) + kill(); + } + public void isKill() { + if( isUnused() ) + kill(); + } + + // 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 = n._inputs.find(this); + n._inputs.set(idx,nnn); + nnn.addUse(n); + CODE.add(n); + CODE.addAll(n._outputs); + } + kill(); + } + + // insert `this` immediately after `def` in the same basic block. + public void insertAfter( Node def ) { + CFGNode cfg = def.cfg0(); + int i = cfg._outputs.find(def)+1; + if( cfg instanceof CallEndNode ) { + cfg = cfg.uctrl(); i=0; + } else if( def.in(0) instanceof MultiNode ) { + assert i==0; + i = cfg._outputs.find(def.in(0))+1; + } + + while( cfg.out(i) instanceof PhiNode || cfg.out(i) instanceof CalleeSaveNode ) i++; + cfg._outputs.insert(this,i); + _inputs.set(0,cfg); + } + + // Insert this in front of use.in(uidx) with this, and insert this + // immediately before use in the basic block. + public void insertBefore( Node use, int uidx ) { + CFGNode cfg = use.cfg0(); + int i; + if( use instanceof PhiNode phi ) { + cfg = phi.region().cfg(uidx); + if( cfg instanceof CProjNode && cfg.in(0) instanceof NeverNode nvr ) + cfg = nvr.cfg0(); + i = cfg.nOuts()-1; + } else { + i = cfg._outputs.find(use); + } + cfg._outputs.insert(this,i); + _inputs.set(0,cfg); + if( _inputs._len > 1 && this instanceof SplitNode ) + setDefOrdered(1,use.in(uidx)); + use.setDefOrdered(uidx,this); + } + + public void setDefOrdered( int idx, Node def) { + // If old is dying, remove from CFG ordered + Node old = in(idx); + if( old!=null && old.nOuts()==1 ) { + CFGNode cfg = old.cfg0(); + if( cfg!=null ) { + cfg._outputs.remove(cfg._outputs.find(old)); + old._inputs.set(0,null); + } + } + setDef(idx,def); + } + + public void removeSplit() { + CFGNode cfg = cfg0(); + cfg._outputs.remove(cfg._outputs.find(this)); + _inputs.set(0,null); + if( _inputs._len > 1 ) subsume(in(1)); + } + + // ------------------------------------------------------------------------ + // 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 = CODE._midAssert ? null : CODE._obs; + if( obs != null ) obs.before(this); + Node n = peepholeOpt(); + Node rez = n==null ? this : deadCodeElim(n._nid >= _nid ? n.peephole() : n); + if( obs != null ) obs.after(this, n==null ? null : rez, false); + return rez; // Cannot return null for no-progress + } + + /** + * 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( ) { + CODE.iterCnt(); + // Compute initial or improved Type + Type old = setType(compute()); + + // Replace constant computations from non-constants with a constant node. + if( !isConst() && !(this instanceof ParmNode parm && parm.inProgress()) && + _type.isHighOrConst() ) + return ConstantNode.make(_type).peephole(); + + // Global Value Numbering + if( _hash==0 ) { + Node n = CODE._gvn.get(this); // Will set _hash as a side effect + if( n==null ) + CODE._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 ) return this; // Report progress; + CODE.iterNop(); + return null; // No progress + } + + // 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) : "Monotonicity test failed: "+this+"#"+_nid+" old="+old+" new="+type; + //assert !type.hasClosedBuilder0(); // New types should not have closed builders + if( old == type ) return old; + _type = type; // Set _type late for easier assert debugging + CODE.addAll(_outputs); + moveDepsToWorklist(); + return old; + } + + @SuppressWarnings("unchecked") + public N init() { _type = compute(); return (N)this; } + + /** + * 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<<2 +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. + Ary _deps; + public int nDeps() { return _deps == null ? 0 : _deps.size(); } + public Node dep(int idx) { return _deps.at(idx); } + + private boolean addDepImpl( Node dep ) { + // Running peepholes during the big assert cannot have side effects + // like adding dependencies. + if( CODE._midAssert ) return false; + if( CODE._obs != null ) CODE._obs.dep(this, dep); + if( dep._deps==null ) dep._deps = new Ary<>(Node.class); + if( dep._deps .find(this) != -1 ) return false; // Already on list + if( dep._outputs.find(this) != -1 ) return false; + return true; + } + + /** + * Add a node to the list of dependencies. Only add it if it's 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. + */ + public N addDep( N dep ) { + if( !addDepImpl(dep) ) return dep; + if( dep._inputs .find(this) != -1 ) return dep; // No need for deps on immediate neighbors + dep._deps.add(this); + return dep; + } + + public N addDepForwards( N dep ) { + if( !addDepImpl(dep) ) return dep; + dep._deps.add(this); + return dep; + } + + // Move the dependents onto a worklist, and clear for future dependents. + public void moveDepsToWorklist( ) { moveDepsToWorklist(CODE._iter); } + public void moveDepsToWorklist( IterPeeps iter ) { + if( _deps == null ) return; + iter.addAll(_deps); + _deps.clear(); + } + + // 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; } + + // ------------------------------------------------------------------------ + // + + /** Is this Node Memory related */ + public boolean isMem() { return false; } + + // ------------------------------------------------------------------------ + // Peephole utilities + + // Swap inputs without letting either input go dead during the swap. + public Node swap12() { + unlock(); // Hash is order dependent + _inputs.swap(1,2); + 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(Node.class); + n._outputs = new Ary<>(Node.class); + n._deps = null; + n._hash = 0; + if( n instanceof CallNode call ) CODE._calls.add(call); + return n; + } + + // Report any post-optimize errors + public Parser.ParseException err() { return null; } + + // Common integer constants + public static ConstantNode con(long x) { return (ConstantNode)ConstantNode.make(TypeInteger.constant(x)).peephole(); } + + // Utility to walk the entire graph applying a function; return the first + // not-null result. + final public E walk( Function pred ) { + assert CODE._visit.isEmpty(); + E rez = _walk(pred); + CODE._visit.clear(); + return rez; + } + + private E _walk( Function pred ) { + if( CODE._visit.get(_nid) ) return null; // Been there, done that + CODE._visit.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; + } + + + // If a Type upgrade happens, put on worklist + boolean _upgradeType( HashMap TYPES ) { return false; } + public final void upgradeType( HashMap TYPES ) { + Type old = _type; _type = _type.upgradeType(TYPES); + boolean progress = _upgradeType(TYPES); + if( progress || old != _type ) { + CODE.add(this); + CODE.addAll(_outputs); + if( _deps!=null ) CODE.addAll(_deps); + } + } + + /** + * Debugging utility to find a Node by index + */ + public Node find(int nid) { return _find(nid, new BitSet()); } + private Node _find(int nid, BitSet bs) { + if( bs.get(_nid) ) return null; // Been there, done that + bs.set(_nid); + if( _nid==nid ) return this; + Node x; + for( Node def : _inputs ) if( def != null && (x = def._find(nid,bs)) != null ) return x; + for( Node use : _outputs ) if( use != null && (x = use._find(nid,bs)) != null ) return x; + return null; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/NotNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NotNode.java new file mode 100644 index 00000000..e22d16b6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NotNode.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class NotNode extends Node { + public NotNode(Node in) { super(null, in); } + + + @Override protected String format() { return "(!%1)"; } + + @Override + public TypeInteger compute() { + Type t0 = in(1)._type; + if( t0.isHigh() ) return (TypeInteger)TypeInteger.BOOL.dual(); + if( t0 == Type.NIL || t0 == TypeInteger.ZERO ) return TypeInteger.TRUE; + if( t0 instanceof TypeNil tn && tn.notNull() ) return TypeInteger.FALSE; + if( t0 instanceof TypeInteger i && (i._min > 0 || i._max < 0) ) return TypeInteger.FALSE; + return TypeInteger.BOOL; + } + + @Override + public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/OrNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/OrNode.java new file mode 100644 index 00000000..263c3e87 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/OrNode.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class OrNode extends ArithNode { + public OrNode( Parser.Lexer loc, Node lhs, Node rhs) { super(loc,lhs, rhs, (byte)1); } + + + @Override public String label() { return "Or"; } + @Override public String op() { return "|"; } + @Override long doOp( long x, long y ) { return x | y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Or of 0. We do not check for (0|x) because this will already + // canonicalize to (x|0) + if( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==0 && t1.isa(TypeInteger.BOT) && forceInt(lhs) ) + 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 super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new OrNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ParmNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ParmNode.java new file mode 100644 index 00000000..316efb93 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ParmNode.java @@ -0,0 +1,95 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class ParmNode extends PhiNode { + + // Argument indices are mapped one-to-one on CallNode inputs + public final int _idx; // Argument index + public Type _declaredType; + + public ParmNode(String label, int idx, Type declaredType, Node... inputs) { + super(label, inputs); + _idx = idx; + _declaredType = declaredType; + _type = declaredType; + } + public ParmNode(ParmNode parm) { super(parm, parm._label ); _idx = parm._idx; _declaredType = parm._declaredType; } + + + @Override public String label() { return MemOpNode.mlabel(_label); } + + @Override boolean _upgradeType( HashMap TYPES) { + Type t = _declaredType.upgradeType(TYPES); + if( t == _declaredType ) return false; + unlock(); + _declaredType = t; + return true; + } + + public FunNode fun() { return (FunNode)in(0); } + + @Override + protected ExprPrinter _print1(ExprPrinter p) { + if( fun()._name!=null && fun()._name.endsWith("") && _label.equals("arg") ) + return p.p("arg"); + p.p("Parm_").p(_label).open(); + for( Node in : _inputs ) p.n(in).p(","); + return p.unchar(',').close(); + } + + + // Always in-progress until we run out of unknown callers + @Override public boolean inProgress() { return in(0) instanceof FunNode fun && fun.inProgress(); } + + @Override + public Type compute() { + if( !(region() instanceof RegionNode r) ) + return region()._type==Type.XCONTROL || region()._type==Type.TOP ? (_type instanceof TypeMem ? TypeMem.TOP : Type.TOP) : _type; + // During parsing Phis have to be computed type pessimistically. + if( r.inProgress() || in(nIns()-1)==null ) + return _declaredType; + // Set type to local top of the starting type + Type t = Type.TOP; + 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. + Type ctrl = addDep(r.in(i))._type; + if( ctrl != Type.XCONTROL && ctrl != Type.TOP ) { + if( in(i)._type==Type.BOTTOM ) + return Type.BOTTOM; + t = t.meet(in(i)._type); + } + } + // Recursion is a loop in disguise. A Parm merges the first argument + // with those from recursive calls, just as a loop Phi merges its entry + // and backedge. Use the widening rule in PhiNode.compute here too. + // CallEndNode.compute handles the separate cycle through return values; + // widening this Parm alone cannot stop f(x-1)+1 from growing its result. + if( t instanceof TypeInteger ti && !ti.isHigh() && !ti.isConstant() ) + t = ti.same_but_slightly_wider_than(); + return t.join(_declaredType); + } + + @Override + public Node idealize() { + if( !(region() instanceof FunNode) ) + // A dead function can lose every caller before the Parm itself is + // removed; there is no replacement input in that transient form. + return nIns()==1 ? null : in(1); // Input has collapse to e.g. starting control. + // If function is folding, do all possible peeps + if( fun()._folding ) return super.idealize(); + + // Skip most phi optimizations on parms + return null; + } + + @Override public boolean eq( Node n ) { + return ((ParmNode)n)._idx==_idx && super.eq(n); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PhiNode.java new file mode 100644 index 00000000..61e4b468 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PhiNode.java @@ -0,0 +1,275 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class PhiNode extends Node { + + public final String _label; + + public PhiNode( String label, Node... inputs) { + super(inputs); + _label = label; + } + public static PhiNode make(String label, Type minType, Node... inputs) { + return minType instanceof TypeMem mem + ? mem._alias==1 + ? new BulkMemPhiNode(label,inputs) + : new MemPhiNode(label,mem._alias,inputs) + : new PhiNode(label, inputs); + } + // Used by ParmNode + public PhiNode(PhiNode phi, String label) { super(phi); _label = label; } + // Used by instruction Selection + public PhiNode(PhiNode phi) { this(phi,phi._label); } + // Used by the infinite-loop exit breaker + public PhiNode(RegionNode r, Node sample) { + super(new Node[]{r}); + _label = ""; + _type = sample._type; + while( nIns() < r.nIns() ) + addDef(sample); + } + + + @Override public String label() { return "Phi_"+MemOpNode.mlabel(_label); } + + @Override + protected ExprPrinter _print1(ExprPrinter p) { + if( !(region() instanceof RegionNode r) || r.inProgress() ) + p.p("Z"); + p.p("Phi("); + for( Node in : _inputs ) p.n(in).p(","); + return p.unchar(',').close(); + } + + public CFGNode region() { return (CFGNode)in(0); } + @Override public boolean isMem() { return _type instanceof TypeMem; } + + @Override + public Type compute() { + if( !(region() instanceof RegionNode r) ) + return Type.TOP; + // During parsing Phis have to be computed type pessimistically. + if( r.inProgress() || in(nIns()-1)==null ) + return Type.BOTTOM; + // Set type to local top of the starting type + Type t = Type.TOP; + 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. + Type ctrl = addDep(r.in(i))._type; + if( ctrl != Type.XCONTROL && ctrl != Type.TOP ) { + if( in(i)._type==Type.BOTTOM ) + return Type.BOTTOM; + t = t.meet(in(i)._type); + } + } + + // Widen loop Phis so integer ranges cannot grow one step per trip forever. + // Recursion is a loop in disguise: ParmNode.compute widens arguments, + // and CallEndNode.compute widens results. Both are needed because the + // recursive result can feed itself without passing through a Parm. + // Above-center ranges may still fall to unwidened constants. + // Widen only below-center ranges to preserve monotonicity. + if( r instanceof LoopNode && // Only around loops + t instanceof TypeInteger ti && // Only widen integers + !ti.isHigh() && !ti.isConstant() ) { // Only widen live ranges + // Widen, to prevent infinite falling of TypeIntegers + return ti.same_but_slightly_wider_than(); + } + + return t; + } + + @Override + public Node idealize() { + RegionNode r = (RegionNode)region(); + if( r.inProgress() || nOuts()==0 ) + return null; // Input is in-progress + + // If we have only a single unique input, become it. + Node live = singleUniqueInput(); + if( live != null ) + return live; + + // No bother if region is going to fold dead paths soon + for( int i=1; i1 ) { + for( Node use : n._outputs ) + if( use!=null && use!=this ) addDepForwards(use); + if( n instanceof StoreNode || busy!=null ) return false; + busy=n; + } + for( int j=1; j _print1(ExprPrinter p) { + if( _label != null ) return p.p(_label); + if( in(0) instanceof CallEndNode cend && cend.call()!=null ) + return p.n(cend.call()); + return p.p("LONELY PROJ"); + } + + @Override public CFGNode cfg0() { + return in(0) instanceof CFGNode cfg ? cfg : in(0).cfg0(); + } + + @Override public boolean isMem() { return _type instanceof TypeMem; } + + @Override + public Type compute() { + Type t = in(0)._type; + return t instanceof TypeTuple tt ? tt._types[_idx] : t.oob(); + } + + @Override public Node idealize() { return ((MultiNode)in(0)).pcopy(_idx,this); } + + @Override + public boolean eq( Node n ) { return _idx == ((ProjNode)n)._idx; } + + @Override + int hash() { return _idx; } + + @Override public int idx() { return _idx; } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntMach.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntMach.java new file mode 100644 index 00000000..9a9b94fd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntMach.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.util.SB; + +/** Zero-code machine form of {@link PtrToIntNode}. */ +public class PtrToIntMach extends PtrToIntNode implements MachNode { + public PtrToIntMach(PtrToIntNode ptr) { super(ptr); } + @Override public String op() { return label(); } + @Override public RegMask regmap(int i) { assert i==1; return RegMask.FULL; } + @Override public RegMask outregmap() { return RegMask.FULL; } + @Override public int twoAddress() { return 1; } + @Override public void encoding(Encoding enc) { } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(in(1))); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java new file mode 100644 index 00000000..094734fa --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; + + +/** + * Reinterpret a non-null managed pointer as its raw integer address. + * + * This node changes representation families but emits no code. No + * control-dependent proof can turn a pointer into an integer; array-to-C-pointer + * conversion adds the array body offset separately. + */ +public class PtrToIntNode extends Node { + public PtrToIntNode(Node ptr) { super(null,ptr); } + public PtrToIntNode(PtrToIntNode ptr) { super(ptr); } + + + @Override protected String format() { return "(i64*)%1"; } + + @Override public Type compute() { + return in(1)._type.isHigh() ? TypeInteger.TOP : TypeInteger.BOT; + } + + @Override public Node idealize() { return null; } + + @Override public Parser.ParseException err() { + Type t = in(1)._type; + if( t instanceof TypeMemPtr ptr ) + return ptr.notNull() ? null : Parser.error("Cannot convert a nullable pointer to an integer",null); + return Parser.error("Cannot convert "+t.str()+" to an integer pointer",null); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyMach.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyMach.java new file mode 100644 index 00000000..a35f5a1d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyMach.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.util.SB; + +/** + * Cast a pointer to read-only + */ +public class ReadOnlyMach extends ReadOnlyNode implements MachNode { + public ReadOnlyMach( ReadOnlyNode n ) { super(n); } + @Override public String op() { return label(); } + @Override public RegMask regmap(int i) { assert i==1; return RegMask.FULL; } + @Override public RegMask outregmap() { return RegMask.FULL; } + @Override public int twoAddress( ) { return 1; } + @Override public void encoding( Encoding enc ) { } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(in(1))); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java new file mode 100644 index 00000000..ab7e0ae0 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMemPtr; + +/** + * Cast a pointer to read-only + */ +public class ReadOnlyNode extends Node { + public ReadOnlyNode( Node n ) { super(null,n); } + public ReadOnlyNode( ReadOnlyNode n ) { super(n); } + + + @Override protected String format() { return "(const)%1"; } + @Override public Type compute() { + Type t = in(1)._type; + return t instanceof TypeMemPtr tmp ? tmp.makeRO() : t; + } + + @Override public Node idealize() { + if( in(1)._type instanceof TypeMemPtr tmp && tmp.isFinal() ) + return in(1); + return null; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/RegionNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/RegionNode.java new file mode 100644 index 00000000..923773c4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/RegionNode.java @@ -0,0 +1,172 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import java.util.*; + +public class RegionNode extends CFGNode { + // Source location for late discovered errors + public Parser.Lexer _loc; + + public RegionNode(Parser.Lexer loc, Node... nodes) { super(nodes); _loc = loc; } + public RegionNode(RegionNode r, Parser.Lexer loc) { super(r); _loc = loc; } + public RegionNode(RegionNode r) { super(r); if( r!=null ) _loc = r._loc; } + + + @Override protected ExprPrinter _print1(ExprPrinter p) { + return p.p(label()); + } + + @Override public boolean blockHead() { return true; } + + @Override + public Type compute() { + if( inProgress() ) return Type.CONTROL; + Type t = Type.XCONTROL; + for (int i = 1; i < nIns(); i++) + t = t.meet(in(i)._type); + return t; + } + + @Override + public Node idealize() { + if( inProgress() ) return null; + + Node progress = deadPath(false); + if( progress!=null ) return progress; + + // If down to a single input, become that input + if( nIns()==2 && !hasPhi() ) + return in(1); // Collapse if no Phis; 1-input Phis will collapse on their own + + // If a CFG diamond with no merging, delete: "if( pred ) {} else {};" + if( !hasPhi() && nIns()>3 && // No Phi users, just a control user + in(1) instanceof CProjNode p1 && + in(2) instanceof CProjNode p2 && + addDepForwards(p1).in(0)==addDepForwards(p2).in(0) && + p1.in(0) instanceof IfNode iff ) { + // Replace with the iff.ctrl directly + if( nIns()==3 ) return iff.ctrl(); + // Just delete the path for fat Regions + setDef(1,iff.ctrl()); + return delDef(2); + } + + // Flatten stacked regions (no loops involved) + if( getClass() == RegionNode.class && nIns()>2 ) { + for( int i=1; i2 && !hasMidUser(region) ) { + assert !region.inProgress(); + // Fold Phis + for( Node use : _outputs ) { + if( use instanceof PhiNode phi ) { + PhiNode phi2 = phi.in(i) instanceof PhiNode phi2x && phi2x.region()==region ? phi2x : null; + for( int j=1; j1 && in(nIns()-1) == null; } + + // Never equal if inProgress + @Override public boolean eq( Node n ) { return !inProgress(); } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReturnNode.java new file mode 100644 index 00000000..0bb090ee --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -0,0 +1,129 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +/** + * 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 CFGNode { + + public FunNode _fun; + + public ReturnNode(Node ctrl, Node mem, Node data, Node rpc, FunNode fun ) { + super(ctrl, mem, data, rpc); + _fun = fun; + } + public ReturnNode( ReturnNode ret, FunNode fun ) { super(ret); _fun = fun; } + + + public Node ctrl() { return in(0); } + public Node mem () { return in(1); } + public Node expr() { return in(2); } + public Node rpc () { return in(3); } + @Override public FunNode fun() { return _fun; } + + @Override + protected ExprPrinter _print1(ExprPrinter p) { + p.p("return "); + if( expr()==null ) p.p("----"); + else p.n(expr()); + return p.p(";"); + } + + // No one unique control follows; can be many call end sites + @Override public CFGNode uctrl() { return null; } + + @Override + public Type compute() { + return TypeTuple.make(ctrl()._type,mem()._type,expr()._type); + } + + @Override public Node idealize() { + if( _fun.isDead() ) return null; + + // Upgrade signature based on return type + Type ret = expr()._type; + TypeFunPtr fcn = _fun.sig(); + if( ret != fcn.ret() && ret.isa(fcn.ret()) ) + _fun.setSig(fcn.makeFrom(ret)); + + return null; + } + + @Override public Parser.ParseException err() { + if( ctrl()._type == Type.CONTROL && + expr()._type == Type.TOP ) + return Parser.error("No defined return type",null); + if( ctrl()._type == Type.CONTROL && + expr()._type == TypeScalar.BOT ) + return Parser.error("Cannot return generic scalar",null); + // With no user constructor, doubles as the public no-arg + // constructor. Required fields remain TOP in its private memory: a + // typed parser poison which deliberately emitted no Store or code. + if( ctrl()._type == Type.CONTROL && _fun.isInstance() ) { + TypeStruct self = ((TypeMemPtr)_fun.sig().arg(0))._obj; + if( !CodeGen.hasUserConstructor(self) && expr()._type instanceof TypeMem mem ) + for( Field fld : self._fields ) { + if( !(fld._t instanceof TypeNil tn && tn.notNull()) ) continue; + Type actual = mem._alias==1 && mem._t instanceof TypeStruct ts + ? ts.field(fld._fname)._t + : mem._alias==fld._alias ? mem._t : Type.TOP; + if( actual==Type.TOP || actual instanceof TypeNil atn && atn.nullable() ) + return Parser.error("'"+self._name+"' is not fully initialized, field '"+ + fld._fname+"' is only partially set in the constructor",null); + } + } + return null; + } + + @Override public boolean eq( Node n ) { + return !_fun.isDead() && !((ReturnNode)n)._fun.isDead(); + } + + // ------------ + // MachNode specifics, shared across all CPUs + public String op() { + return _fun._frameAdjust > 0 ? "epilog" : "ret "; + } + // Correct Nodes outside the normal edges + public void postSelect(CodeGen code) { + _fun.setRet(this); + } + public RegMask regmap(int i) { + return i==2 + ? CodeGen.CODE._mach.retMask(_fun.sig()) + : CodeGen.CODE._retMasks[i]; + } + public RegMask outregmap() { return null; } + public void encoding( Encoding enc ) { throw Utils.TODO(); } + public void asm(CodeGen code, SB sb) { + int frameAdjust = fun()._frameAdjust; + if( frameAdjust>0 ) + sb.p("\n").p("rsp += #").p(frameAdjust).p("\nret "); + // Post code-gen, just print the "ret" + if( code._phase.ordinal() < CodeGen.Phase.RegAlloc.ordinal() || + (code._phase.ordinal() == CodeGen.Phase.RegAlloc.ordinal() && !code._regAlloc.done()) ) + // Prints return reg (either RAX or XMM0), RPC and then the + // callee-save registers. + for( int i=2; i>"; } + @Override long doOp( long x, long y ) { return x >> y; } + @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { + if( y.isConstant() ) { + int log = (int)y.value(); + return TypeInteger.make(-1L<<(63-log),(1L<<(63-log))-1); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + + Type t2 = rhs._type; + + // Sar of 0. + if( t2.isConstant() && t2 instanceof TypeInteger i && (i.value()&63)==0 && forceInt(lhs) && lhs._type instanceof TypeInteger ) + return lhs; + + // TODO: x >> 3 >> (y ? 1 : 2) ==> x >> (y ? 4 : 5) + + return super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new SarNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ScopeNode.java new file mode 100644 index 00000000..694d5dcb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -0,0 +1,719 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Var; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CompUnit; +import com.seaofnodes.simple.codegen.ParseAll; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import java.util.*; + +/** + * The Scope node is purely a parser helper - it tracks names to nodes with a + * stack of variables. + */ +public class ScopeNode extends MemMergeNode { + + /** + * 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"; + public static final String MEM0 = "$mem"; + + // All active/live variables in all nested scopes, all run together. + // Maps 1-to-1 to the _inputs array. + public final Ary _vars; + + // Lexical scope is typed: + public static class Kind { + // Basic block scoping + public static class Block extends Kind { } + // Constructor/initializer block for an allocated struct. + public static class Constructor extends Block { + public Constructor(TypeStruct self) { _self = self; } + public final TypeStruct _self; + } + // Function scope + public static class Func extends Kind { + public final String _name; + public final boolean _constructor; + public Func(String name) { this(name,false); } + public Func(String name, boolean constructor) { _name=name; _constructor=constructor; } + } + + public int _lexSize; // Number of Vars in this scope + } + public final Ary _kinds; + // Lexical scope nesting depth + public int depth() { return _kinds._len; } + public Kind klast() { return _kinds.last(); } + + // Extra guards; tested predicates and cast results + private final Ary _guards; + + // A new ScopeNode + public ScopeNode() { + super(); + _vars = new Ary<>(Var .class); + _kinds = new Ary<>(Kind.class); + _guards = new Ary<>(Node.class); + } + + @Override public String label() { return "Scope"; } + + @Override + protected ExprPrinter _print1(ExprPrinter p) { + p.p("Scope[ "); + int j=1; + for( int i=0; i N ctrl(N n) { return setDef(0,n); } + public Node mem(Node n) { return setDef(1,n); } + + public void push( Kind kind ) { + kind._lexSize = _vars.size(); + _kinds.push(kind); + } + + // Pop a lexical scope + public void pop() { + promote(null); // Promote forward references to the next outer scope + _pop(); + } + // Pop a scope, no promote FRefs + public void _pop() { + int n = _kinds.pop()._lexSize; + popUntil(n); // Pop off inputs going out of scope + _vars.setLen(n); // Pop off variables going out of scope + } + + + // Look for forward references in the last lexical scope and promote to the + // next outer lexical scope. At the last scope declare them an error. + public void promote(CodeGen code) { + Kind kind = klast(); + int n = kind._lexSize; + for( int i=n; i=0; i-- ) + if( _kinds.at(i) instanceof Kind.Constructor ctor ) + return ctor._self; + else if( _kinds.at(i) instanceof Kind.Func func && FunNode.isInstance(func._name) ) + return ((TypeMemPtr)var(_kinds.at(i)._lexSize).type())._obj; + return null; + } + + // Find nearest enclosing function scope + public int enclosingFunction() { + for( int i=depth()-1; i>=0; i-- ) + if( _kinds.at(i) instanceof Kind.Func func && !FunNode.isInstance(func._name) ) + return i; + throw Utils.TODO("Should not reach here"); + } + + // Find nearest enclosing function or declaration scope + public int enclosingFuncOrDecl() { + for( int i=depth()-1; i>=0; i-- ) + if( _kinds.at(i) instanceof Kind.Func func ) + return i; + throw Utils.TODO("Should not reach here"); + } + + // Which lexical scope contains this Var? + public int kindx( Var v ) { + for( int i=depth()-1; i>=0; i-- ) + if( v._idx >= _kinds.at(i)._lexSize ) + return i; + return -1; + } + + // Which lexical scope contains this Var? + + + public int kindFcnx( Var v ) { + for( int i=depth()-1; i>=0; i-- ) + if( v._idx >= _kinds.at(i)._lexSize && _kinds.at(i) instanceof Kind.Func ) + return i; + throw Utils.TODO("Should not reach here"); + } + + // Which function scope contains this Var? + public Kind kindFcn( Var v ) { + int kx = kindFcnx(v); + return kx == -1 ? null : _kinds.at(kx); + } + + // Final fields are writable local values inside their own initializer or + // constructor. Final parameters, local variables, and captured bindings + // remain final. Use the same rule when deciding which loop Phis to build. + public boolean canInit(Var v) { + int fx=enclosingFuncOrDecl(); + Kind.Func fun=(Kind.Func)_kinds.at(fx); + if( !fun._constructor || v._extern!=0 ) return false; + int dx=FunNode.isInstance(fun._name) ? fx : fx-1; + return dx>=0 && kindx(v)==dx && v._idx>=_kinds.at(dx)._lexSize+2; + } + + private boolean loopVar(Var v) { + return !v._final || (canInit(v) && !v.type().isHighOrConst()); + } + + // Find name in reverse, return an index into _vars or -1. Linear scan + // instead of hashtable, but probably doesn't matter until the scan + // typically hits many dozens of variables. + public int find( String name ) { + for( int i=_vars.size()-1; i>=0; i-- ) + if( var(i)._name.equals(name) ) + return i; + return -1; + } + + /** + * Create a new variable name in the current scope + */ + public boolean define( String name, Type declaredType, boolean xfinal, Node init, Parser.Lexer loc ) { + assert _kinds.isEmpty() || name!=MEM0 ; // Later scopes do not define memory + if( depth() > 0 ) + for( int i=_vars.size()-1; i>=klast()._lexSize; i-- ) { + Var n = var(i); + if( n._name.equals(name) ) { + if( !n.isFRef() ) return false; // Double define + FRefNode fref = (FRefNode)in(n._idx); // Get forward ref + if( !xfinal || !declaredType.isConstant() ) throw fref.err(); // Must be a final constant + n.defFRef(declaredType,xfinal,loc); // Declare full correct type, final, source location + setDef(n._idx,fref.addDef(init)); // Set FRef to defined; tell parser also + return true; + } + } + Var v = new Var(nIns(),name,declaredType,xfinal,loc,init==CodeGen.CODE.XCTRL); + _vars.add(v); + // Creating a forward reference + if( init==CodeGen.CODE.XCTRL ) + //init = new FRefNode(v).init(); + throw Utils.TODO(); + addDef(init); + return true; + } + + // Normal lookup was tried and failed. Append a forward ref in the current + // lexical scope. Scope pops promote it outwards "as if" the final constant + // had been declared before the using scope. + + public Var defineFRef( String id, Type t, boolean xfinal, Parser.Lexer loc ) { + Var var = new Var(nIns(),id,t,xfinal,loc,true); + FRefNode fref = new FRefNode(id,loc).init(); + fref._type = fref._con = t; + _vars.add(var); + addDef(fref); + return var; + } + + + // Parser state carries one complete, conservative memory value. Precise + // alias partitions belong to the graph and are introduced by peepholes. + public Node mem( int alias ) { + assert alias == 1; + return in(update(var(1),null)); + } + public void mem( int alias, Node st ) { + assert alias == 1; + update(var(1),st); + } + + + /** + * Lookup a name in all scopes starting from most deeply nested. + * + * @param name Name to be looked up + * @return null if not found, or the implementing Node + */ + public Var lookup( String name ) { + int idx = find(name); + // -1 is missed in all scopes, not found + return idx == -1 ? null : update(var(idx),null); + } + + /** + * Redefine an existing name + * + * @param name Name being redefined + * @param n The node to bind to the name + */ + public void update( String name, Node n ) { + int idx = find(name); + assert idx>=0; + update(var(idx),n); + } + + public Var update( Var v, Node st ) { + Node old = in(v._idx); + if( old instanceof ScopeNode loop ) { + // Lazy Phi! + Node def = loop.in(v._idx); + old = def instanceof PhiNode phi && loop.ctrl()==phi.region() + // Loop already has a real Phi, use it + ? def + // 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(v._idx,PhiNode.make(v._name, v.type(), loop.ctrl(), loop.in(loop.update(v,null)._idx),null).init()); + setDef(v._idx,old); + } + //assert !v._final || st==null; + if( st!=null ) setDef(v._idx,st); // Set new value + return v; + } + + /** + * 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 an 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 + dup._vars .addAll(_vars ); + dup._kinds .addAll(_kinds ); + dup._guards .addAll(_guards ); + // The dup'd guards all need dup'd keepers, to keep proper accounting + // when later removing all guards + for( Node n : _guards ) + if( !(n instanceof CFGNode) ) + n.keep(); + dup.addDef(ctrl()); // Control input is just copied + + // Memory is one ordinary parser-state value. At loop heads the + // Scope sentinel triggers the same lazy-Phi machinery as variables. + dup.addDef(loop ? this : mem()); + + // Copy of other inputs + for( int i=2; i 0 ) { + Node cast = _guards.at(--i); + if( cast instanceof CFGNode ) continue; // Marker between guard sets + Node xpred = _guards.at(--i); + if( xpred == pred ) + return cast; + } + return pred; + } + + // Kill guards also + @Override public void kill() { + for( Node n : _guards ) + if( !(n instanceof CFGNode) ) + n.unkill(); + _guards.clear(); + // Can have lazy uses remaining + if( isUnused() ) + super.kill(); + } + + + private void replace( Node old, Node cast, boolean nonZero ) { + assert old!=null && old!=cast; + for( int i=0; i>shf) != x._min || + ((x._max<>shf) != x._max ) ) + return TypeInteger.make(x._min< (x << i) + (c << i) + if( lhs instanceof AddNode add && addDep(add).in(2)._type instanceof TypeInteger c && c.isConstant() ) { + long sum = c.value() << shl.value(); + if( Integer.MIN_VALUE <= sum && sum <= Integer.MAX_VALUE ) + return new AddNode(new ShlNode(_loc,add.in(1),rhs).peephole(), CodeGen.CODE.con(sum) ); + } + } + + // TODO: x << 3 << (y ? 1 : 2) ==> x << (y ? 4 : 5) + + return super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new ShlNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShrNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShrNode.java new file mode 100644 index 00000000..17170a1f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShrNode.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class ShrNode extends ArithNode { + public ShrNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + + + @Override public String op() { return ">>>"; } + @Override long doOp( long x, long y ) { return x >>> y; } + @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { + return x == TypeInteger.ZERO ? x : TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t2 = rhs._type; + + // Shr of 0. + if( t2.isConstant() && t2 instanceof TypeInteger i && (i.value()&63)==0 && forceInt(lhs) && lhs._type instanceof TypeInteger ) + return lhs; + + // TODO: x >>> 3 >>> (y ? 1 : 2) ==> x >>> (y ? 4 : 5) + + return super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new ShrNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/SplitNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SplitNode.java new file mode 100644 index 00000000..3042f857 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SplitNode.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.LRG; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; + +public abstract class SplitNode extends MachConcreteNode { + public final LRG _lrg; // Live range for split + public final String _kind; // Kind of split + public final byte _round; // Debugging info for which RegAlloc round + public SplitNode(LRG lrg,String kind, byte round, Node[] nodes) { super(nodes); _lrg=lrg; _kind = kind; _round = round; } + // A register-only transfer may need another split to reach stack storage. + public boolean isNonStackSplit() { return false; } + @Override public String op() { return "mov"; } + @Override protected ExprPrinter _print1(ExprPrinter p) { + p.p("mov("); + if( in(1) == null ) p.p("---"); + else p.n(in(1)); + return p.close(); + } + public FunNode fun(Encoding enc) { return enc._code._regAlloc.lrg(this)._fun; } + @Override public Type compute() { return in(0)._type; } + @Override public Node idealize() { return null; } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))); + } + @Override public String comment() { return _kind + " #"+ _round; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartCUNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartCUNode.java new file mode 100644 index 00000000..0f7a322a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartCUNode.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Start of a single compilation unit, distinct from the outer whole-program Start. +public class StartCUNode extends StartNode { + public final String _fname; + public StartCUNode(StartNode start, StopCUNode stop, Type arg, String fname) { super(start,stop,arg); _fname = fname; } + public StartCUNode(StartCUNode start) { super(start); _fname = start._fname; } + + + @Override public CFGNode cfg0() { return (CFGNode)in(0); } + + // IDom is cfg0() and depth is always Start+1 which is 0+1 or 1 + @Override public int idepth() { return 1; } + @Override public CFGNode idom(Node dep) { return cfg0(); } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartNode.java new file mode 100644 index 00000000..3c605e30 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartNode.java @@ -0,0 +1,87 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.Arrays; +import java.util.HashSet; + +/** + * The Start node represents the external world calling, be it calling the + * entry or just some linked caller calling. + *

+ * Start initially has 1 input (arg) from outside and the initial control. + * In ch10 we also add mem aliases as structs get defined; each field in struct + * adds a distinct alias to Start's tuple. + *

+ * By ch25, Start is the external world (or linker) calling with everything + * that has escaped. Start is NOT the entry itself, which can assume + * no memory nor fidxs exist nor escaped. Start gets escaped things from Stop; + * together these close the Grand Cycle between the code we know and the code + * we don't. + */ +public class StartNode extends LoopNode implements MultiNode { + + final Type _arg; + + public StartNode(StartNode start, StopNode stop, Type arg) { super(null,start,stop); _arg = arg; } + public StartNode(StartNode start) { super(start); _arg = start==null ? null : start._arg; } + + public Type arg() { return _arg; } + + @Override protected ExprPrinter _print1(ExprPrinter p) { + return p.p(label()); + } + + @Override public CFGNode cfg0() { return null; } + + // Get the one control following; error to call with more than one e.g. an + // IfNode or other multi-way branch. For Start, it's the class init for + // this compilation unit. + @Override public CFGNode uctrl() { + // Find module., it's the start. + CFGNode C = null; + for( Node use : _outputs ) + if( use instanceof FunNode fun && CodeGen.CODE.entryClinitName().equals(fun._name) ) + { assert C==null; C = fun; } + if( C==null ) + for( FunNode fun : CodeGen.CODE._linker ) + if( fun!=null && !fun.isDead() && CodeGen.CODE.entryClinitName().equals(fun._name) ) + { assert C==null; C = fun; } + return C; + } + + boolean escapedFIDX(int fidx) { + // Public class fields are callable by the unknown external world + // regardless of the current SCCP approximation of Start memory. + if( CodeGen.CODE.publicFIDX(fidx) ) return true; + if( !(_type instanceof TypeTuple tt) ) + return !_type.isHigh(); // TOP never escapes, BOTTOM always escapes + TypeMem tmem = (TypeMem)tt._types[1]; + if( tmem == TypeMem.BOT ) return false; + if( tmem == TypeMem.TOP ) return false; + if( XInt.isHigh(tmem._escFs) ) + return false; + return XInt.bit(tmem._escFs,fidx); + } + + @Override public TypeTuple compute() { + StopNode stop = (StopNode)in(1); + TypeMem tmem = stop._type instanceof TypeTuple tt + ? tt.mem() + : stop._type.isHigh() ? TypeMem.START : TypeMem.BOT; + assert tmem._alias==1 && !tmem._one; + return TypeTuple.make(Type.CONTROL,tmem,_arg); + } + + @Override public Node idealize() { return null; } + + // No immediate dominator, and idepth==0 + @Override public int idepth() { return 0; } + @Override public CFGNode idom(Node dep) { return null; } + + @Override public boolean eq( Node n ) { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopCUNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopCUNode.java new file mode 100644 index 00000000..53c2325c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopCUNode.java @@ -0,0 +1,126 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Stop of a single compilation unit, distinct from the outer whole-program Stop. +public class StopCUNode extends StopNode { + public StopCUNode() { super(); } + public StopCUNode(StopNode stop) { super(stop); } + + + public StartCUNode start() { + for( Node use : _outputs ) + if( use instanceof StartCUNode start ) + return start; + return null; + } + + @Override + public TypeTuple compute() { + // During Parsing, new Stops and Returns are being added. This Stop + // cannot know if more are coming, so must assume the worst. + if( CodeGen.CODE._phase ==null || CodeGen.CODE._phase == CodeGen.Phase.Parse ) + return TypeTuple.STATE; + + StartCUNode start = start(); + // Memory start from all returns + TypeMem xmem = TypeMem.START; + // Escape summaries are monotone discovery facts. In Opto, a Return + // can widen from a precise set to FULL; the public-only normalization + // below must not forget private bits already discovered precisely from + // that Return on an earlier SCCP step. + if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() && + CodeGen.CODE._phase.ordinal() <= CodeGen.Phase.Lower.ordinal() && + _type instanceof TypeTuple old && old.mem() instanceof TypeMem omem ) + xmem = xmem.makeFrom(XInt.meet(xmem._escFs,omem._escFs), + XInt.meet(xmem._escAs,omem._escAs)); + + for( Node def : _inputs ) { + ReturnNode ret = (ReturnNode)def; + if( ret._type == Type.TOP ) continue; + if( !(ret._type instanceof TypeTuple tret) ) + return TypeTuple.STATE; // Must be Type.BOTTOM + + FunNode fun = ret.fun(); + boolean noUserCtor = fun.isInstance() && + !CodeGen.hasUserConstructor(((TypeMemPtr)fun.sig().arg(0))._obj); + // Instance is an allocation helper, never an externally + // callable constructor when a user constructor exists. Otherwise + // the same no-arg is the public constructor. + boolean pub = fun.isPublic() && (fun.isClz() || noUserCtor); + TypeFunPtr sig = fun.sig(); + // If public init, the function escapes and the related class escapes + if( pub ) { + xmem = xmem.escapes(sig); + // Classes also escape their public clazz pointer, which then + // escapes all their public fields. Private fields do not + // escape, and are only available from the local class code. + if( fun.isClz() ) + xmem = xmem.escapesAliases(sig._sig[0]); + } + + // The instance Start edge supplies conservative pre-Opto + // inputs, but its private-memory result is consumed only by the + // allocation sequence and is never an externally visible return. + if( fun.isInstance() && !noUserCtor ) continue; + + // Does this function escape? If not, then no need to MEET + // here as its effects are not visible to the outside world. + addDepForwards(start); // Check again if function escapes + if( pub || start.escapedFIDX(sig.fidx()) ) { + // External callers already hold their object arguments, so + // writes through those arguments expose addresses too. + for( Node use : fun._outputs ) + if( use instanceof ParmNode p && p._idx>=2 ) + xmem=publicBottom(xmem.escapes(addDepForwards(p)._type)); + if( tret.ctl().isHigh() ) continue; // Return not reachable + // Capture (precisely?) all escaping pointer aliases and fidxs. + // Escaped fidxs means the linked world can call that function; + // escaped aliases means the linked world can R/W those aliases. + xmem = xmem.escapes(tret.ret()); + xmem = publicBottom(xmem); + xmem = (TypeMem)xmem.meet(publicBottom(tret.mem(),xmem)); + } + } + // Native retention is shared by the whole program, including calls + // on paths with no Return. A class boundary must not hide it. + for( CallNode call : CodeGen.CODE._calls ) + if( !call.isDead() ) xmem=publicBottom(xmem.escapes(call.exposed(this))); + assert xmem._alias==1 && !xmem._one; + return TypeTuple.make(Type.CONTROL,xmem,Type.BOTTOM); + } + + // FULL escape sets arise from global BOTTOM. Across a compilation-unit + // boundary global BOTTOM exposes every public field, while retaining any + // private escapes already discovered precisely. + private TypeMem publicBottom(TypeMem mem) { return publicBottom(mem,mem); } + private TypeMem publicBottom(TypeMem mem, TypeMem prior) { + // The pessimistic Iter pass naturally lifts transient FULL inputs to + // precise finite sets. Replacing FULL early there would let a later + // input refinement add bits, reversing Iter's monotonic direction. + // This normalization is needed only for Opto's optimistic grand cycle. + if( CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal() || + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.Lower.ordinal() ) return mem; + int[] escFs = XInt.isHigh(mem._escFs) + ? XInt.meet(prior._escFs,CodeGen.CODE.publicFIDXs()) + : mem._escFs; + int[] escAs = XInt.isHigh(mem._escAs) + ? XInt.meet(prior._escAs,CodeGen.CODE.publicAliases()) + : mem._escAs; + return escFs==mem._escFs && escAs==mem._escAs ? mem : mem.makeFrom(escFs,escAs); + } + + @Override + public Node idealize() { + int len = nIns(); + for( int i=0; i _print1(ExprPrinter p) { + // For the sake of many old tests, and single value prints as "return val" + ReturnNode ret1 = null; + for( Node n : _inputs ) { + if( n instanceof ReturnNode ret ) { + if( ret1 != null ) { ret1=null; break; } + ret1 = ret; + } + } + if( ret1!=null ) return p.n(ret1); + + p.p("Stop[ "); + for( Node n : _inputs ) + if( n instanceof ReturnNode ret ) { + String name = ret.fun()._name; + if( name== null || !name.startsWith("sys.") ) + p.n(ret).p(" "); + } + return p.p("]"); + } + + @Override public boolean blockHead() { return true; } + + + // If a single Return, return it. + // Otherwise, null because ambiguous. + public ReturnNode ret() { + ReturnNode ret1 = null; + for( Node n : _inputs ) { + // TODO: remove isPublic + if( n instanceof ReturnNode ret && ret.fun().isPublic() ) { + if( ret1 != null ) return null; // Ambiguous + ret1 = ret; + } + } + return ret1; + } + + @Override + public TypeTuple compute() { + // During Parsing, new Stops and Returns are being added. This Stop + // cannot know if more are coming, so must assume the worst. + if( CodeGen.CODE._phase ==null || CodeGen.CODE._phase == CodeGen.Phase.Parse ) + return TypeTuple.STATE; + + // Just meet-over-inputs. + TypeTuple tt = TypeTuple.STOP_HIGH; + for( Node def : _inputs ) + tt = (TypeTuple)tt.meet(def._type); + assert tt.mem()._alias==1 && !tt.mem()._one; + return tt; + } + + @Override public Node idealize() { + int len = nIns(); + for( int i=0; i _print1(ExprPrinter p) { + return p.p(".").p(_name).p("=").p( val()).p(";"); + } + + @Override + public Type compute() { + Type val = val()._type; + Type mem0 = mem()._type; + Type ptr0 = ptr()._type; + // Validate argument types + if( ptr0.isHigh() ) + return TypeMem.TOP; + if( !(mem0 instanceof TypeMem mem) || + !(ptr0 instanceof TypeMemPtr ptr) ) + return mem0.isHigh() || ptr0==Type.NIL ? TypeMem.TOP : TypeMem.BOT; + + // Sharpen memory value; required for narrowing stores where the parser + // inserts zero/sign masking and somebody reads the TypeMem type. + Type decl = declaredType(); + if( decl != Type.BOTTOM ) + { assert decl != Type.TOP; val = val.join(decl); } + // A singleton class's final constant field lives in the static image. + // Its precise memory slice is a constant too; ordinary peepholes can + // replace this Store before scheduling or instruction selection. + // A constant runtime value alone is not enough: mutable fields (notably + // $init) still need their writes, and instance fields need allocation. + Field fld = ptr._obj.field(_name); + if( _alias!=1 && ptr._one && Parser.startsClzPrefix(ptr._obj._name) && + fld!=null && fld._final && decl.isConstant() ) + return TypeMem.make(_alias,val,true,true,true,mem._escFs,mem._escAs).escapesFrom(val); + + // Allocation uses a private TypeMem and nothing else does. This + // memory is truly private; a temporary singleton until it escapes - + // which is never does in a constructor. + if( !_name.equals("[]") && mem._one ) + // Just track the stored value + return TypeMem.make(_alias,val,true,false,_init,null,null).escapesFrom(val); + + // Normal aliasing Store. + assert mem._alias==1 || mem._alias==_alias; // Perfect aliasing + + // Single alias store. If memory is fat, attempt to get a narrow slice. + Type tfld = mem._alias==1 && mem._t instanceof TypeStruct ts + ? ts._fields[ts.findAlias(_alias)]._t + : mem._t; + + // Same alias, meet into other fields. + Type t = val.meet(tfld); + // Bulk memory's final bit describes its conservative remainder, not + // this newly precise slice. Slice finality comes from the field. + boolean xfinal = fld != null && fld._final; + return TypeMem.make(_alias,t,mem._one,mem._clz,xfinal,mem._escFs,mem._escAs).escapesFrom(val); + } + + @Override + public Node idealize() { + // Freeze the semantic Store variant as soon as the pointer exposes a + // canonical field declaration. Waiting until after structural memory + // peeps can lose the declaration to a flow-sensitive constant field + // state. + if( _size==0 && (_size= storageType()) != 0 ) { + unlock(); // Width participates in the cached hash. + return init(); + } + + assert !(mem()._type instanceof TypeMem mem) || _alias == 1 || mem._alias == 1 || _alias == mem._alias; + + // Simple store-after-MemMerge to a known alias can bypass. Happens when inlining. + Node mem = mem(); + if( _alias != 1 && mem instanceof MemMergeNode merge ) { + setDef(1,merge.alias(_alias)); + return this; + } + + Field fld; + if( _alias==1 && ptr()._type instanceof TypeMemPtr tmp && (fld=tmp._obj.field(_name)) != null && _size != 0 ) { + // Expand Store(bulkMem,alias#1) into MemMerge(bulkMem,#N:Store(bulkMem,alias#N)). + + // Normally I'd like to just drop in this xform and move on, but + // users of the bulk Store memory might nontheless depend on the + // sharp memory type (e.g. I64) from the store's memory. Using the + // MemMerge means using the bulk memory which e.g. might be BOT. + // Check for store users with the same alias, and use the new store + // directly, preserving the sharper graph. + Node st = new StoreNode(_loc,_name,fld._alias,fld._t,in(0),mem,ptr(),off(),val(),_init,_size).peephole(); + for( int i=0; i= store_size && not_affected_bits >= store_size) { + setDef(4, shl.in(1)); + return this; + } + } + + return null; + } + + // Flow-sensitive pointer types can carry a current field value (e.g. the + // constant zero in a u32 field). Storage width comes from the canonical + // struct declaration. If the pointer/definition is not available yet, + // leave the Store undecided instead of consulting the value or `_con`. + private byte storageType() { + if( !(ptr()._type instanceof TypeMemPtr tmp) ) + return 0; + TypeStruct obj = (TypeStruct)Parser.TYPES.get(tmp._obj._name); + Field fld = obj.field(_name); + if( fld == null || fld._t == null ) + return 0; + if( fld._t == Type.BOTTOM ) + return 0; // Store into a final field + assert fld._t != Type.TOP; + // Freshly parsed field stores retain the declaring structure on their + // symbolic offset even after the pointer's flow type sharpens. + if( fld._t.isConstant() ) { + if( !(off() instanceof ConFldOffNode coff) ) + return 0; + fld = coff._ts.field(_name); + if( fld == null || fld._t == null ) + return 0; + } + // Compute storage size from field type + byte size = (byte)(1 << fld._t.log_size()); + assert size==1 || size==2 || size==4 || size==8; + return size; + } + + // Alias required by a direct consumer of this Store's memory result. + // Keep in sync with BulkMemPhiNode.outputAlias. + private int useAlias(Node use) { + return switch( use ) { + case MemOpNode mem -> mem._alias; + case MemPhiNode phi -> phi._alias; + case EscapeNode esc -> esc.pub()==this ? esc.fld()._alias : 0; + default -> 0; + }; + } + + // Phi(mem, Store(phi,ptr,off,val)) -> Store(mem,ptr,off,Phi(load(mem),val)). + // With no observers of the backedge store, only the last value matters. + // The entry load preserves the field even when the loop takes zero trips. + Node sink(MemPhiNode phi) { + phi.addDepForwards(this); + if( nOuts()!=1 || _alias==1 || _size==0 || _init ) return null; + phi.addDep(ptr()); + // Start with a fixed field of an allocation dominating the loop. + // In particular, do not sink stores through a loop-varying address. + if( !(ptr() instanceof ProjNode p) || !(p.in(0) instanceof NewNode obj) || + !(ptr()._type instanceof TypeMemPtr tmp) || tmp._obj.isAry() || + !(off() instanceof ConstantNode) || err()!=null ) return null; + LoopNode loop = (LoopNode)phi.region(); + // Bulk splitting identifies precise slices by Region/alias. Finish it + // before replacing this memory point with a Store. + for( Node use : loop._outputs ) + if( use instanceof BulkMemPhiNode ) { + phi.addDepForwards(use); + return null; + } + phi.addDep(obj); + CFGNode ctrl = loop.entry(); + while( ctrl!=null && ctrl!=obj.cfg0() ) { + phi.addDep(ctrl); + ctrl=ctrl.idom(phi); + } + if( ctrl==null ) return null; + + Node mem = phi.in(1); + Node init = new LoadNode(_loc,_name,_alias,null,mem,ptr(),off()).peephole(); + Node value = CodeGen.CODE.add(new PhiNode(_name,loop,init,val()).peephole()); + Node sink = new StoreNode(_loc,_name,_alias,_con,null,mem,ptr(),off(),value,_init,_size); + // Break the memory cycle, killing the old store. Keep the Phi alive + // even if that store was its last use, until our caller replaces it. + phi.keep(); + phi.setDef(2,mem); + phi.unkeep(); + return sink; + } + + // Check that "mem" has no uses except "this" + private boolean checkOnlyUse(Node mem) { + if( mem.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 : mem._outputs ) + if( use != this ) + addDep(use); + return false; + } + + @Override + public Parser.ParseException err() { + Parser.ParseException err = super.err(); + if( err != null ) return err; + assert _size>0 || CodeGen.CODE._phase.ordinal() <= CodeGen.Phase.FinalizeIR.ordinal(); + if( ptr()._type == Type.TOP ) // Dead store? + return null; + TypeMemPtr tmp = (TypeMemPtr)ptr()._type; + Field f = tmp._obj.field(_name); + if( f!=null && (tmp._ro || f._final) && !_init ) + return Parser.error("Cannot modify final field '"+_name+"'",_loc); + return null; + } + + // Do not GVN Stores. Two equal Stores on two of three paths are the easy + // failure: sharing them makes GCM place the Store on all three paths. + // Sharing is safe only if every exit path goes through an original Store, + // or GCM splits the shared Store to keep it on those paths. Stores must + // sink, not run on extra paths. GVN does not check this; GCM does not split. + // Identity also disables generic Phi factoring of Stores. + // The closed-loop Store sink is a separate peep and still applies. + @Override public boolean eq(Node n) { return this == n; } + @Override int hash() { return super.hash() ^ _size; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/SubNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SubNode.java new file mode 100644 index 00000000..adad2ed5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SubNode.java @@ -0,0 +1,56 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class SubNode extends ArithNode { + @Override boolean allowFloat() { return true; } + public SubNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + public SubNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs,mode); } + + + @Override public String op() { return "-"; } + + @Override long doOp( long x, long y ) { return x - y; } + @Override double doOp(double x, double y ) { return x - y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + // Sub of same is 0 + if( in(1)==in(2) ) + return TypeInteger.ZERO; + // Fold ranges like {2-3} - {0-1} into {1-3}. + if( !AddNode.overflow(x._min,-y._max) && + !AddNode.overflow(x._max,-y._min) && + y._min != Long.MIN_VALUE ) + return TypeInteger.make(x._min-y._max,x._max-y._min); + + 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 and unresolved numeric modes for type checking. + if( _mode==1 && 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 super.idealize(); + } + + @Override Node copy(Node lhs, Node rhs) { return new SubNode(lhs,rhs,_mode); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java new file mode 100644 index 00000000..1acf55b6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java @@ -0,0 +1,26 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; + + +public class ToFloatNode extends Node { + public ToFloatNode(Node lhs) { super(null, lhs); } + + + @Override protected String format() { return "(flt)%1"; } + + @Override + public Type compute() { + if( in(1)._type.isHigh() ) return TypeFloat.F64.dual(); + if( in(1)._type == Type.NIL ) return TypeFloat.FZERO; + if( in(1)._type instanceof TypeInteger i0 && i0.isConstant() ) + return TypeFloat.constant(i0.value()); + return TypeFloat.F64; + } + + @Override public Node idealize() { return null; } + @Override Node copy(Node lhs, Node rhs) { return new ToFloatNode(lhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java new file mode 100644 index 00000000..9136aacc --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; + +// Truncate toward zero, saturating overflow; NaN converts to zero. +public class ToIntegerNode extends Node { + public ToIntegerNode(Node lhs) { super(null,lhs); } + + @Override public String label() { return "ToInteger"; } + @Override protected String format() { return "(int)%1"; } + + @Override public Type compute() { + Type t = in(1)._type; + if( t.isHigh() ) return TypeInteger.TOP; + if( t instanceof TypeFloat tf && tf.isConstant() ) + return TypeInteger.constant((long)tf.value()); + return TypeInteger.BOT; + } + + @Override public Node idealize() { return null; } + @Override Node copy(Node lhs, Node rhs) { return new ToIntegerNode(lhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/TypeNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/TypeNode.java new file mode 100644 index 00000000..970472da --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/TypeNode.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.BitSet; +import java.util.HashMap; + +// Node carries an extra internal type +public abstract class TypeNode extends Node { + public Type _con; + public TypeNode(Type t, Node... nodes) { super(nodes); assert t!=null; _con = t; } + public TypeNode(TypeNode tn) { super(tn); _con = tn._con; } + // Copy edges from ideal, but 'this' is a Type-Mach-Node (e.g. MemOpX86) and needs a _con + public TypeNode(Node ideal, Type con) { super(ideal); _con = con; } + + // Upgrade the internal type + @Override boolean _upgradeType( HashMap TYPES) { + return liftType(_con.upgradeType(TYPES)); + } + + // Lift type + public boolean liftType( Type t ) { + if( t == _con ) return false; + unlock(); // Unlock before changing _con + _con = t; + return true; + } + + @Override public boolean eq(Node n) { return _con==((TypeNode)n)._con; } + @Override int hash() { return _con.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java new file mode 100644 index 00000000..3792e224 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; + +public class XCtrlNode extends CFGNode { + public XCtrlNode() { super(new Node[]{CodeGen.CODE._start}); } + + @Override protected ExprPrinter _print1(ExprPrinter p) { return p.p("Xctrl"); } + @Override public boolean isConst() { return true; } + @Override public Type compute() { return Type.XCONTROL; } + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/XorNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/XorNode.java new file mode 100644 index 00000000..e9cd13a5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/XorNode.java @@ -0,0 +1,43 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class XorNode extends ArithNode { + public XorNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + + + @Override public String op() { return "^"; } + @Override long doOp( long x, long y ) { return x ^ y; } + @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Xor of 0. We do not check for (0^x) because this will already + // canonicalize to (x^0) + if( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==0 && forceInt(lhs) && lhs._type instanceof TypeInteger ) + 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 super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new XorNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddARM.java new file mode 100644 index 00000000..76e47ca2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddARM.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddARM extends MachConcreteNode implements MachNode { + AddARM( Node add) { super(add); } + @Override public String op() { return "add"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + // ADD (shifted register) + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_ADD); } + // General form: "rd = rs1 + rs2" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + ").p(code.reg(in(2))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddFARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddFARM.java new file mode 100644 index 00000000..67a10b48 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddFARM.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddFARM extends MachConcreteNode implements MachNode { + AddFARM(Node addf) { super(addf); } + @Override public String op() { return "addf"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + + //FADD (scalar) + @Override public void encoding( Encoding enc ) { arm.f_scalar(enc,this,Arm64.OPF_OP_ADD);} + + // Default on double precision for now(64 bits) + // General form: "addf rd = src1 + src2 + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + ").p(code.reg(in(2))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddIARM.java new file mode 100644 index 00000000..4c635d07 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddIARM.java @@ -0,0 +1,28 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; + +public class AddIARM extends MachConcreteNode implements MachNode { + final int _imm; + AddIARM(Node add, int imm) { + super(add); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { + return _imm == 1 ? "inc" : (_imm == -1 ? "dec" : "addi"); + } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + //ADD (immediate) + @Override public void encoding( Encoding enc ) { + arm.imm_inst(enc,this, in(1), Arm64.OPI_ADD,_imm); + } + // General form: "addi rd = rs1 + imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndARM.java new file mode 100644 index 00000000..1a0365ad --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndARM.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class AndARM extends MachConcreteNode implements MachNode { + AndARM(Node and) { super(and); } + @Override public String op() { return "and"; } + @Override public String glabel() { return "&"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + // AND (shifted register) + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_AND); } + // General form: #rd = rs1 & rs2 + @Override public void asm(CodeGen code, SB sb){ + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" & ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndIARM.java new file mode 100644 index 00000000..6292e98e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndIARM.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AndIARM extends MachConcreteNode implements MachNode { + final int _imm; + AndIARM(Node and, int imm) { + super(and); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "andi"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + arm.imm_inst_n(enc,this, in(1), Arm64.OPI_AND,_imm); + } + // General form: "andi rd = rs1 & imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" & #").p(Arm64.decodeImm12(_imm)); + } +} \ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrARM.java new file mode 100644 index 00000000..77f14d4c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrARM.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Arithmetic Shift Right (register) +public class AsrARM extends MachConcreteNode implements MachNode { + AsrARM(Node asr) {super(asr);} + @Override public String op() { return "sar"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public void encoding( Encoding enc ) { arm.shift_reg(enc,this,Arm64.OP_ASR); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >> ").p(code.reg(in(2))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrIARM.java new file mode 100644 index 00000000..7cb42eec --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrIARM.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class AsrIARM extends MachConcreteNode implements MachNode { + final int _imm; + AsrIARM(Node asr, int imm) { + super(asr); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "asri"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + short rd = enc.reg(this); + short rn = enc.reg(in(1)); + assert _imm > 0; + enc.add4(Arm64.imm_shift(Arm64.OPI_ASR,_imm, 0b111111, rn, rd)); + } + // General form: "asri rd = rs1 >> imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >> #").p(_imm); + } +} \ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java new file mode 100644 index 00000000..31bbde86 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java @@ -0,0 +1,80 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Jump on flags, uses flags +public class BranchARM extends IfNode implements MachNode, RIPRelSize { + String _bop; + final boolean _fp; + BranchARM(IfNode iff, String bop, boolean fp ) { + super(iff); + _bop = bop; + _fp = fp; + } + @Override public String op() { return "j"+_bop; } + @Override public String label() { return op(); } + + @Override public void postSelect(CodeGen code) { + Node set = in(1); + if( set==null ) return; // Never-node cutout + Node cmp = set.in(1); + // Bypass an expected Set and just reference the cmp directly + if( set instanceof SetARM) + _inputs.set(1,cmp); + else + throw Utils.TODO(); + } + + @Override public RegMask regmap(int i) { assert i==1; return arm.FLAGS_MASK; } + @Override public RegMask outregmap() { return null; } + @Override public void negate() { _bop = negate(_bop); } + + @Override public void encoding( Encoding enc ) { + // Assuming that condition flags are already set. These flags are set + // by comparison (or sub). No need for regs because it uses flags + if( in(1)!=null ) { + // B.cond + enc.add4( Arm64.b_cond(Arm64.OP_BRANCH, 0, Arm64.make_condition(_bop, _fp)) ); + } else { + if( _bop=="!=" ) return; // Inverted, no code + enc.add4(Arm64.b(Arm64.OP_UJMP, 0)); + } + enc.jump(this,cproj(0)); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( in(1)==null && _bop=="!=" ) return 0; // Inverted never-node, no code + if( -(1<<19) <= delta && delta < (1<<19) ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + assert !( in(1)==null && _bop=="!=" ); // Inverted never-node, no code no patch + if( opLen==4 ) { + enc.patch4(opStart,Arm64.b_cond(Arm64.OP_BRANCH, delta, Arm64.make_condition(_bop, _fp))); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + if( in(1)!=null ) { // Never-node + String src = code.reg(in(1)); + if( src!="flags" ) sb.p(src).p(" "); + } else if( _bop=="!=" ) { + sb.p("never"); + return; + } + CFGNode prj = cproj(0).uctrlSkipEmpty(); + if( !prj.blockHead() ) prj = prj.cfg0(); + sb.p(label(prj)); + } + @Override public String comment() { return "L"+cproj(1)._nid; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java new file mode 100644 index 00000000..4de28f3a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java @@ -0,0 +1,50 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; + +public class CallARM extends CallNode implements MachNode, RIPRelSize { + final TypeFunPtr _tfp; + final String _name; + CallARM(CallNode call, TypeFunPtr tfp) { + super(call); + _inputs.pop(); // Pop constant target + assert tfp.isConstant(); + _tfp = tfp; + _name = CodeGen.CODE.funcName(tfp.fidx()); + } + + @Override public String op() { return "call"; } + @Override public String label() { return op(); } + @Override public String name() { return _name; } + @Override public TypeFunPtr tfp() { return _tfp; } + @Override public RegMask regmap(int i) { return arm.callInMask(_tfp,i,fun()._maxArgSlot); } + @Override public RegMask outregmap() { return null; } + @Override public int nargs() { return nIns()-2; } // Minus control, memory, fptr + + @Override public void encoding( Encoding enc ) { + FunNode fun = CodeGen.CODE.link(_tfp); + if( fun==null ) enc.external(this,_name); + else enc.relo(this); + // BL + enc.add4(Arm64.b(Arm64.OP_CALL,0)); // Target patched at link time + } + + // Delta is from opcode start, but X86 measures from the end of the 5-byte encoding + @Override public byte encSize(int delta) { return 4; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + enc.patch4(opStart,Arm64.b(Arm64.OP_CALL,delta)); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(_name).p(" "); + for( int i=0; i= 1L<<12 ) throw Utils.TODO(); + enc.add4(Arm64.imm_inst(Arm64.OPI_SUB, sz, arm.RSP, arm.RSP)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/I2F8ARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/I2F8ARM.java new file mode 100644 index 00000000..b1c30afc --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/I2F8ARM.java @@ -0,0 +1,26 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class I2F8ARM extends MachConcreteNode implements MachNode { + I2F8ARM(Node i2f8) { super(i2f8); } + @Override public String op() { return "cvt"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public void encoding( Encoding enc ) { + // SCVTF + short self = (short)(enc.reg(this )-arm.D_OFFSET); + short reg1 = enc.reg(in(1)); + int body = Arm64.float_cast(Arm64.OP_FLOAT_C, 1, reg1, self); + enc.add4(body); + } + + // General form: "i2f8 (flt)int_value" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p("(flt)").p(code.reg(in(1))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java new file mode 100644 index 00000000..72b76edd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Integer constants +public class IntARM extends ConstantNode implements MachNode { + final String _ext; + IntARM( ConstantNode con, String ext ) { super(con); _ext = ext; } + + @Override public String op() { return "ldi"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public boolean isClone() { return true; } + @Override public IntARM copy() { return new IntARM(this,_ext); } + + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); + short self = enc.reg(this); + long x = _con==Type.NIL ? 0 : ((TypeInteger)_con).value(); + Arm64.constant(enc,self,x); + } + + // Human-readable form appended to the SB. Things like the encoding, + // indentation, leading address or block labels not printed here. + // Just something like "ld4\tR17=[R18+12] // Load array base". + // General form: "op\tdst=src+src" + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + _con.print(sb.p(reg).p(" = #")); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java new file mode 100644 index 00000000..53494092 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java @@ -0,0 +1,34 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.LoadNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +// Load memory addressing on ARM +// Support imm, reg(direct), or reg+off(indirect) addressing +// Base = base - base pointer, offset is added to base +// idx = null +// off = off - offset added to base + +public class LoadARM extends MemOpARM { + LoadARM(LoadNode ld, Node base, Node idx, int off) { + super(ld, base, idx, off, 0); + } + @Override public String op() { return "ld"+_sz; } + @Override public RegMask outregmap() { return size() >= 4 ? arm.MEM_MASK : arm.WMASK; } + + private static final int[] OP_LOADS = new int[]{ Arm64.OP_LOAD_IMM_8, Arm64.OP_LOAD_IMM_16, Arm64.OP_LOAD_IMM_32, Arm64.OP_LOAD_IMM_64, }; + + private static final int[] OP_LOAD_RS = new int[]{ Arm64.OP_LOAD_R_8 , Arm64.OP_LOAD_R_16 , Arm64.OP_LOAD_R_32 , Arm64.OP_LOAD_R_64, }; + + // ldr(immediate - unsigned offset) | ldr(register) + @Override public void encoding( Encoding enc ) { + ldst_encode(enc, OP_LOADS[Integer.numberOfTrailingZeros(size())], OP_LOAD_RS[_con.log_size()], this, size()); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(","); + asm_address(code,sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslARM.java new file mode 100644 index 00000000..954cfac2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Logical Shift Left (register) +public class LslARM extends MachConcreteNode implements MachNode { + LslARM(Node asr) {super(asr);} + @Override public String op() { return "shr"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { arm.shift_reg(enc,this,Arm64.OP_LSL); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" << ").p(code.reg(in(2))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslIARM.java new file mode 100644 index 00000000..b32153ab --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslIARM.java @@ -0,0 +1,34 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class LslIARM extends MachConcreteNode implements MachNode { + final int _imm; + LslIARM(Node lsl, int imm) { + super(lsl); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "lsli"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + short rd = enc.reg(this); + short rn = enc.reg(in(1)); + assert _imm > 0; + // UBFM , , #(- MOD 64), #(63-) + // immr must be (- MOD 64) = 64 - shift + enc.add4(Arm64.imm_shift(Arm64.OPI_LSL, 64 - _imm, (64 - _imm) - 1, rn, rd)); + } + // General form: "lsli rd = rs1 << imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" << #").p(_imm); + } +} \ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrARM.java new file mode 100644 index 00000000..91be34e3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Logical Shift Right (register) +public class LsrARM extends MachConcreteNode implements MachNode { + LsrARM(Node asr) {super(asr);} + @Override public String op() { return "shr"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { arm.shift_reg(enc,this,Arm64.OP_LSR); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >>> ").p(code.reg(in(2))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrIARM.java new file mode 100644 index 00000000..243ce968 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrIARM.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// logical right shift immediate +public class LsrIARM extends MachConcreteNode implements MachNode { + final int _imm; + LsrIARM(Node lsr, int imm) { + super(lsr); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "lsri"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + short rd = enc.reg(this); + short rn = enc.reg(in(1)); + assert _imm > 0; + enc.add4(Arm64.imm_shift(Arm64.OPI_LSR,_imm, 0b111111, rn,rd)); + } + // General form: "lsri rd = rs1 >>> imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >>> #").p(_imm); + } +} \ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java new file mode 100644 index 00000000..14eaf510 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java @@ -0,0 +1,81 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import java.lang.StringBuilder; + +public abstract class MemOpARM extends MemOpNode implements MachNode { + final int _off; // Limit 9 bits sized, or (13 bits< _printMach(ExprPrinter p) { return p.p(".").p(_name); } + + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } + + int size() { return _bytes; } + + // Wider mask to store both GPRs and FPRs + @Override public RegMask regmap(int i) { + // 0 - ctrl + if( i==1 ) return null; // memory + if( i==2 ) return arm.RMASK; // ptr/base + if( i==3 ) return arm.RMASK; // off/index + if( i==4 ) return size() >= 4 ? arm.MEM_MASK : arm.RMASK; // value + return null; // Anti-dependence + } + + // Shared encoding for loads and stores(int and float) + public void ldst_encode( Encoding enc, int opcode_imm, int opcode_reg, Node xval, int size) { + short ptr = enc.reg(ptr()); + short off = enc.reg(off()); + short val = enc.reg(xval); + // The allocated register selects integer vs SIMD/FP encoding, even for + // bitwise copies. ARM numbers each register bank from zero in the opcode. + boolean fp = val >= arm.D0; + assert !fp || size >= 4; + if( fp ) { + opcode_imm |= 1<<4; + opcode_reg |= 1<<5; + val -= arm.D0; + } + int body = off() == null + ? Arm64.load_str_imm(opcode_imm, _off, ptr, val, size) + : Arm64.indr_adr(opcode_reg, off, Arm64.STORE_LOAD_OPTION.SXTX, 0, ptr, val); + enc.add4(body); + } + + SB asm_address(CodeGen code, SB sb) { + sb.p("[").p(code.reg(ptr())).p("+"); + if( off() != null ) sb.p(code.reg(off())); + else sb.p(_off); + return sb.p("]"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulARM.java new file mode 100644 index 00000000..f598cefb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// mulh signed multiply instruction(no-imm form) +public class MulARM extends MachConcreteNode implements MachNode { + MulARM(Node mul) {super(mul);} + @Override public String op() { return "mul"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { arm.madd(enc,this,Arm64.OP_MUL,31); } + // General form: "rd = rs1 * rs2" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulFARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulFARM.java new file mode 100644 index 00000000..a4f81fd4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulFARM.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MulFARM extends MachConcreteNode implements MachNode{ + MulFARM(Node mulf) {super(mulf);} + @Override public String op() { return "mulf"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public void encoding( Encoding enc ) { arm.f_scalar(enc,this,Arm64.OPF_MUL); } + // Default on double precision for now(64 bits) + // General form: "VMUL.f32 rd = src1 * src2 + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NegARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NegARM.java new file mode 100644 index 00000000..881b41f9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NegARM.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +public class NegARM extends MachConcreteNode implements MachNode { + NegARM(Node sub) { + super(sub); + //_inputs.pop(); + } + @Override public String op() { return "neg"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + // reverse subtract with immediate 0 + arm.imm_inst(enc,this, in(1), 0b00100110, 0); + } + // General form: "subi rd = rs1 - imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = -").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java new file mode 100644 index 00000000..8bb8983e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class NewARM extends NewNode implements MachNode, RIPRelSize { + // A pre-zeroed chunk of memory. + NewARM(NewNode nnn) { super(nnn); } + @Override public void encoding( Encoding enc ) { + // BL(branch with link) + enc.external(this,"calloc"). + add4(Arm64.mov(Arm64.OP_MOVZ, 0, 1, 0)). // movz x0,#1 + add4(Arm64.b(Arm64.OP_CALL, 0)); + } + + // Patch is for running "new" in a JIT. + // Delta is from opcode start + @Override public byte encSize(int delta) { return 4*2; } + + // Patch is for running "new" in a JIT. + // Delta is from opcode start + @Override public void patch(Encoding enc, int opStart, int opLen, int delta ) { + // Negative patches are JIT emulator targets. + if( opStart+delta < 0 ) { + enc.patch4(opStart+4, Arm64.b_calloc(Arm64.OP_CALL, delta-4)); + } else { + throw Utils.TODO(); + } + } + // General form: "alloc #bytes PC" + @Override public void asm(CodeGen code, SB sb) { + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); + sb.p("call #calloc"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NotARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NotARM.java new file mode 100644 index 00000000..f8e450f6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NotARM.java @@ -0,0 +1,28 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.util.SB; + +public class NotARM extends MachConcreteNode implements MachNode{ + NotARM(NotNode not) {super(not);} + @Override public String op() { return "not"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public RegMask killmap() { return arm.FLAGS_MASK; } + @Override public void encoding( Encoding enc ) { + // subs xzr, rs, #0 + // cset rd, eq // Set rd to 1 if rs == 0 (equal), else 0 + // subtracting zero from rs will just yield rs, it sets the zero flag and then it's used in cset + short self = enc.reg(this ); + short reg1 = enc.reg(in(1)); + int subs = Arm64.imm_inst(Arm64.OP_SUBS, 0, reg1, 31); + enc.add4(subs); + int cset = Arm64.cset(Arm64.OP_CSET, 31, Arm64.COND.EQ, 31, self); + enc.add4(cset); + } + + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrARM.java new file mode 100644 index 00000000..5b7264ec --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrARM.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Not using the ORRS variant. +public class OrARM extends MachConcreteNode implements MachNode { + OrARM(Node and) { super(and); } + @Override public String op() { return "or"; } + @Override public String glabel() { return "|"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_OR); } + // General form: #rd = rs1 & rs2 + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" | ").p(code.reg(in(2))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrIARM.java new file mode 100644 index 00000000..86d07bee --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrIARM.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class OrIARM extends MachConcreteNode implements MachNode { + final int _imm; + OrIARM(Node or, int imm) { + super(or); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "ori"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + arm.imm_inst_n(enc, this, in(1), Arm64.OPI_OR, _imm); + } + // General form: "ori rd = rs1 | imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" | #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ParmARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ParmARM.java new file mode 100644 index 00000000..b419fe0a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ParmARM.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ParmNode; + +public class ParmARM extends ParmNode implements MachNode { + final RegMask _rmask; + ParmARM(ParmNode parm) { + super(parm); + _rmask = arm.callInMask(fun().sig(),_idx,0); + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return _rmask; } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ProjARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ProjARM.java new file mode 100644 index 00000000..4b7d6f13 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ProjARM.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ProjNode; + +public class ProjARM extends ProjNode implements MachNode { + ProjARM( ProjNode p ) { super(p); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { + return ((MachNode)in(0)).outregmap(_idx); + } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/RetARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/RetARM.java new file mode 100644 index 00000000..a7ed5073 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/RetARM.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; + +public class RetARM extends ReturnNode implements MachNode { + RetARM(ReturnNode ret, FunNode fun) { super(ret, fun); fun.setRet(this); } + @Override public void encoding( Encoding enc ) { + int frameAdjust = ((FunARM)fun())._frameAdjust; + if( frameAdjust > 0 ) + enc.add4(Arm64.imm_inst(Arm64.OPI_ADD, frameAdjust, arm.RSP, arm.RSP)); + enc.add4(Arm64.ret(Arm64.OP_RET)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java new file mode 100644 index 00000000..e5b06c39 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// conditional set? +// e.g CSET, stores flag info into GPRS after compare +public class SetARM extends MachConcreteNode implements MachNode { + final String _bop; // One of <,<=,== + final boolean _fp; // FP compare (unsigned encoding) + SetARM(Node cmp, String bop, boolean fp) { + super(cmp); + _inputs.setLen(1); // Pop the cmp inputs + // Replace with the matched cmp + _inputs.push(cmp); + _bop = bop; + _fp = fp; + } + @Override public String op() { return "set"+_bop; } + @Override public RegMask regmap(int i) { assert i==1; return arm.FLAGS_MASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { + // Using RM==RN==31 implies a *zero* value, not R31. + int body = Arm64.cset(Arm64.OP_CSET,0b11111, Arm64.make_condition(_bop, _fp), 0b011111, enc.reg(this)); + enc.add4(body); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)); + String src = code.reg(in(1)); + if( src!="flags" ) sb.p(" = ").p(src); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java new file mode 100644 index 00000000..dcfeb5d8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.SplitNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class SplitARM extends SplitNode { + SplitARM(LRG lrg, String kind, byte round) { super(lrg,kind,round,new Node[2]);} + @Override public RegMask regmap(int i) { return arm.SPLIT_MASK; } + @Override public RegMask outregmap() { return arm.SPLIT_MASK; } + + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + + boolean dstX = dst >= arm.D_OFFSET; + boolean srcX = src >= arm.D_OFFSET; + + if (dst >= arm.MAX_REG) { + // store to sp + if(src >= arm.MAX_REG) { + throw Utils.TODO(); + } + int off = fun(enc).computeStackOffset(enc._code,dst); + if( srcX ) src -= arm.D_OFFSET; + enc.add4(Arm64.load_str_imm(Arm64.OP_STORE_IMM_64, off, arm.RSP, src, 8)); + return; + } + + if(src >= arm.MAX_REG) { + // Load from SP + int off = fun(enc).computeStackOffset(enc._code,src); + if( dstX ) dst -= arm.D_OFFSET; + enc.add4(Arm64.load_str_imm(Arm64.OP_LOAD_IMM_64, off, arm.RSP, dst, 8)); + return; + } + + // pick opcode based on regs + if(!dstX && !srcX) { + // GPR->GPR + enc.add4(Arm64.mov_reg(Arm64.OP_MOV, src, dst)); + } else if(dstX && srcX) { + // FPR->FPR + // fmov reg + enc.add4(Arm64.f_mov_reg(Arm64.OP_FMOV_REG, src - arm.D_OFFSET,dst - arm.D_OFFSET)); + } else if(!srcX && dstX) { + // GPR->FPR + // FMOV(general) 64 bits to DOUBLE-PRECISION + enc.add4(Arm64.f_mov_general(Arm64.OP_FMOV, 0b01, 0, 0b111, src, dst - arm.D_OFFSET)); + } else if( srcX && !dstX) { + //FPF->GPR + // FMOV(general) DOUBLE-PRECISION to 64 bits + enc.add4(Arm64.f_mov_general(Arm64.OP_FMOV, 0b01, 0, 0b110, src - arm.D_OFFSET, dst)); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java new file mode 100644 index 00000000..25e5d501 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; + +// NZCV transfers require a GPR. Further splits can spill that GPR to the stack. +public class SplitFlagsARM extends SplitARM { + private static final RegMask MASK = new RegMask(arm.WR_BITS,1L); + SplitFlagsARM(LRG lrg,String kind,byte round) { super(lrg,kind,round); } + @Override public boolean isNonStackSplit() { return true; } + @Override public RegMask regmap(int i) { return MASK; } + @Override public RegMask outregmap() { return MASK; } + + @Override public void encoding(Encoding enc) { + short dst=enc.reg(this), src=enc.reg(in(1)); + if( dst==src ) return; + if( src==arm.FLAGS ) enc.add4(Arm64.flags(dst,false)); + else if( dst==arm.FLAGS ) enc.add4(Arm64.flags(src,true)); + else super.encoding(enc); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java new file mode 100644 index 00000000..c9b2127a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.*; + +// Keep a saved NZCV value in GPRs and stack slots through subsequent spills. +public class SplitGPRARM extends SplitARM { + private static final RegMask MASK = new RegMask(arm.WR_BITS,-2L); + SplitGPRARM(LRG lrg,String kind,byte round) { super(lrg,kind,round); } + @Override public RegMask regmap(int i) { return MASK; } + @Override public RegMask outregmap() { return MASK; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java new file mode 100644 index 00000000..0b90ac8b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StoreNode; +import com.seaofnodes.simple.util.SB; + +// Store memory addressing on ARM +// Support imm, reg(direct), or reg+off(indirect) addressing +// base - base pointer, offset is added to base +// null - index never allowed (no [reg+reg] mode) +// off - offset added to base +// imm - immediate value to store, only if val is null +// val - value to store or null + +//e.g s.cs[0] = 67; // C +// base = s.cs, off = 4, imm = 67, val = null +public class StoreARM extends MemOpARM { + StoreARM(StoreNode st, Node base, Node idx, int off, Node val) { + super(st, base, idx, off, 0, val); + } + @Override public String op() { return "st"+_sz; } + @Override public RegMask outregmap() { return null; } + + private static final int[] OP_STORES = new int[]{ Arm64.OP_STORE_IMM_8, Arm64.OP_STORE_IMM_16, Arm64.OP_STORE_IMM_32, Arm64.OP_STORE_IMM_64, }; + + private static final int[] OP_STORE_RS = new int[]{ Arm64.OP_STORE_R_8, Arm64.OP_STORE_R_16, Arm64.OP_STORE_R_32, Arm64.OP_STORE_R_64, }; + + @Override public void encoding( Encoding enc ) { + ldst_encode(enc, OP_STORES[Integer.numberOfTrailingZeros(size())], OP_STORE_RS[Integer.numberOfTrailingZeros(_bytes)], val(), size()); + } + @Override public void asm(CodeGen code, SB sb) { + asm_address(code,sb).p(","); + if( val()==null ) sb.p("#").p(_imm); + else sb.p(code.reg(val())); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubARM.java new file mode 100644 index 00000000..fd5cd4de --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class SubARM extends MachConcreteNode implements MachNode { + SubARM(Node sub) { super(sub); } + @Override public String op() { return "sub"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_SUB); } + + // General form: "sub # rd = rs1 - rs2" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" - ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubFARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubFARM.java new file mode 100644 index 00000000..1a751434 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubFARM.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class SubFARM extends MachConcreteNode implements MachNode { + SubFARM(Node subf) {super(subf);} + @Override public String op() { return "subf"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public void encoding( Encoding enc ) { arm.f_scalar(enc,this,Arm64.OPF_SUB); } + // Default on double precision for now(64 bits) + // General form: "vsub.f32 rd = src1 + src2 + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" - ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubIARM.java new file mode 100644 index 00000000..b2c0b2ed --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubIARM.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +public class SubIARM extends MachConcreteNode implements MachNode { + final int _imm; + SubIARM(Node sub, int imm) { + super(sub); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return _imm == -1 ? "dec" : "subi"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { + arm.imm_inst(enc,this, in(1), Arm64.OPI_SUB,_imm); + } + + // General form: "subi rd = rs1 - imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" - #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java new file mode 100644 index 00000000..a053ba46 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java @@ -0,0 +1,51 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class TFPARM extends FunPtrNode implements MachNode, RIPRelSize { + final String _ext; + // Pointer to a Simple function + TFPARM( FunPtrNode fptr ) { super(fptr); _ext = null; } + // Pointer to an Extern "C" function + TFPARM( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } + @Override public String op() { return "ldx"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public boolean isClone() { return true; } + @Override public TFPARM copy() { return new TFPARM((TypeFunPtr)_con,_ext); } + @Override public void encoding( Encoding enc ) { + if( _ext==null ) enc.relo(this); // Internal patch + else enc.external(this,_ext); // ELF-file external patch + short dst = enc.reg(this); + // adrp x0, 0 + enc.add4(Arm64.adrp(1,0, Arm64.OP_ADRP, 0,dst)); + // add x0, x0, 0 + arm.imm_inst(enc,Arm64.OPI_ADD,0, dst); + } + + @Override public byte encSize(int delta) { + return 8; + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short rpc = enc.reg(this); + if(opLen == 8 ) { + arm.patch_adrp_add(enc, opStart, delta, rpc); + } else { + // should not happen as one instruction is 4 byte, and TFP arm encodes 2. + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + _type.print(sb.p(reg).p(" #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java new file mode 100644 index 00000000..b2531f97 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class TMPARM extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + TMPARM( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ldp"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public boolean isClone() { return true; } + @Override public TMPARM copy() { return new TMPARM(this,_ext); } + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1); + short dst = enc.reg(this); + // adrp x0, 0 + enc.add4(Arm64.adrp(1,0, Arm64.OP_ADRP, 0,dst)); + // add x0, x0, 0 + arm.imm_inst(enc,Arm64.OPI_ADD,0, dst); + } + + @Override public byte encSize(int delta) { + return 8; + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short dst = enc.reg(this); + if(opLen == 8 ) { + arm.patch_adrp_add(enc, opStart, delta, dst); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + _con.print(sb.p(reg).p(" #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/UJmpARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/UJmpARM.java new file mode 100644 index 00000000..aecd02bb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/UJmpARM.java @@ -0,0 +1,48 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// unconditional jump +public class UJmpARM extends CFGNode implements MachNode, RIPRelSize { + UJmpARM() { } + @Override public String op() { return "jmp"; } + @Override public String label() { return op(); } + @Override protected String format() { return "jmp "; } + @Override public RegMask regmap(int i) {return null; } + @Override public RegMask outregmap() { return null; } + @Override public Type compute() { throw Utils.TODO(); } + @Override public Node idealize() { throw Utils.TODO(); } + @Override public void encoding( Encoding enc ) { + enc.jump(this,uctrl()); + int body = Arm64.b(Arm64.OP_UJMP, 0); + enc.add4(body); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( -(1<<26) <= delta && delta < (1<<26) ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + if( opLen==4 ) { + enc.patch4(opStart,Arm64.b(Arm64.OP_UJMP, delta)); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + CFGNode target = uctrl(); + //assert target.nOuts() > 1; // Should optimize jmp to empty targets + sb.p(label(target)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorARM.java new file mode 100644 index 00000000..d56397a3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class XorARM extends MachConcreteNode implements MachNode{ + XorARM(Node xor) {super(xor);} + @Override public String op() { return "xor"; } + @Override public String glabel() { return "^"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_XOR); } + // General form: "rd = x1 ^ x2" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ^ ").p(code.reg(in(2))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorIARM.java new file mode 100644 index 00000000..12143ccd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorIARM.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; + +public class XorIARM extends MachConcreteNode implements MachNode { + final int _imm; + XorIARM(Node xor, int imm) { + super(xor); + _inputs.pop(); + _imm = imm; + } + + @Override public String op() { return "xori"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + // General form: "xori rd = rs1 ^ imm" + @Override public void encoding( Encoding enc ) { arm.imm_inst_n(enc,this, in(1), Arm64.OPI_XOR,_imm); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ^ #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java new file mode 100644 index 00000000..3c965da5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java @@ -0,0 +1,503 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import static com.seaofnodes.isa.Arm64.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class arm extends Machine { + public arm( CodeGen code ) { + if( !"SystemV".equals(code._callingConv) ) + throw new IllegalArgumentException("Unknown calling convention "+code._callingConv); + } + + // ARM64 + @Override public String name() { return "arm"; } + + // GPR(S) + public static final int X0 = 0, X1 = 1, X2 = 2, X3 = 3, X4 = 4, X5 = 5, X6 = 6, X7 = 7; + public static final int X8 = 8, X9 = 9, X10 = 10, X11 = 11, X12 = 12, X13 = 13, X14 = 14, X15 = 15; + public static final int X16 = 16, X17 = 17, X18 = 18, X19 = 19, X20 = 20, X21 = 21, X22 = 22, X23 = 23; + public static final int X24 = 24, X25 = 25, X26 = 26, X27 = 27, X28 = 28, X29 = 29, X30 = 30, RSP = 31; + + // Floating point registers + public static final int D0 = 32, D1 = 33, D2 = 34, D3 = 35, D4 = 36, D5 = 37, D6 = 38, D7 = 39; + public static final int D8 = 40, D9 = 41, D10 = 42, D11 = 43, D12 = 44, D13 = 45, D14 = 46, D15 = 47; + public static final int D16 = 48, D17 = 49, D18 = 50, D19 = 51, D20 = 52, D21 = 53, D22 = 54, D23 = 55; + public static final int D24 = 56, D25 = 57, D26 = 58, D27 = 59, D28 = 60, D29 = 61, D30 = 62, D31 = 63; + + static final int FLAGS = 64; + static final int MAX_REG = 65; + public static final int D_OFFSET = 32; + + static final String[] REGS = new String[] { + "X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7", + "X8", "X9", "X10", "X11", "X12", "X13", "X14", "X15", + "X16", "X17", "X18", "X19", "X20", "X21", "X22", "X23", + "X24", "X25", "X26", "X27", "X28", "X29", "RPC", "RSP", + "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", + "D8", "D9", "D10", "D11", "D12", "D13", "D14", "D15", + "D16", "D17", "D18", "D19", "D20", "D21", "D22", "D23", + "D24", "D25", "D26", "D27", "D28", "D29", "D30", "D31", + "flags" + }; + @Override public String[] regs() { return REGS; } + + // from (x0-x30) + // General purpose register mask: pointers and ints, not floats + static final long RD_BITS = 0xFFFFFFFFL; + static final RegMask RMASK = new RegMask(RD_BITS); + static final long WR_BITS = 0x7FFFFFFFL; // All the GPRs, not RSP + static final RegMask WMASK = new RegMask(WR_BITS); + + // Float mask from(d0–d31) + static final long FP_BITS = 0xFFFFFFFFL<>55) == ti.value(); + } + // True if signed 12-bit immediate + private static boolean imm12(TypeInteger ti) { + // 52 = 64-12 + return ti.isConstant() && ((ti.value()<<52)>>52) == ti.value(); + } + + // Can we encode this in ARM's 12-bit LOGICAL immediate form? + // Some combination of shifted bit-masks. + private static int imm12Logical(TypeInteger ti) { + return ti.isConstant() ? Arm64.logicalImmediate(ti.value()) : -1; + } + + public static void imm_inst(Encoding enc, Node n,Node n2, int opcode, int imm12) { + short self = enc.reg(n); + short reg1 = enc.reg(n2); + + // ADD/SUB encode an unsigned magnitude; switch operation for a negative addend. + if( imm12<0 && (opcode==OPI_ADD || opcode==OPI_SUB) ) { + opcode ^= OPI_ADD ^ OPI_SUB; + imm12 = -imm12; + } + int body = Arm64.imm_inst(opcode, imm12&0xFFF, reg1, self); + enc.add4(body); + } + + public static void imm_inst_subs(Encoding enc, Node n,Node n2, int opcode, int imm12) { + short reg1 = enc.reg(n2); + // 31 = 11111 + int body = Arm64.imm_inst(opcode, imm12&0xFFF, reg1, 31); + enc.add4(body); + } + + // for cases where rs1 and dst are the same, eg add x0, x0, 1 + public static void imm_inst(Encoding enc, int opcode, int imm12, int self) { + int body = Arm64.imm_inst(opcode, imm12&0xFFF, self, self); + enc.add4(body); + } + + public static void imm_inst_n(Encoding enc, Node n, Node n2, int opcode, int imm13) { + short self = enc.reg(n); + short reg1 = enc.reg(n2); + int body = Arm64.imm_inst_n(opcode, imm13, reg1, self); + enc.add4(body); + } + + // for cases where rs1 and dst are the same, eg add x0, x0, 1 + public static int imm_inst_l(Encoding enc, Node n, int opcode, int imm12) { + short self = enc.reg(n); + short reg1 = enc.reg(n.in(1)); + int body = Arm64.imm_inst(opcode, imm12&0xFFF, reg1, self); + return body; + } + + // for normal add, reg1, reg2 cases (reg-to-reg) + // using shifted-reg form + + public static void r_reg(Encoding enc, Node n, int opcode) { + short self = enc.reg(n); + short reg1 = enc.reg(n.in(1)); + short reg2 = enc.reg(n.in(2)); + int body = Arm64.r_reg(opcode, 0, reg2, 0, reg1, self >= 32 ? reg1: self); + enc.add4(body); + } + + public static void r_reg_subs(Encoding enc, Node n, int opcode) { + short reg1 = enc.reg(n.in(1)); + short reg2 = enc.reg(n.in(2)); + // 31 = 11111 + int body = Arm64.r_reg(opcode, 0, reg2, 0, reg1, 31); + enc.add4(body); + } + + public static void shift_reg(Encoding enc, Node n, int op2) { + short self = enc.reg(n); + short reg1 = enc.reg(n.in(1)); + short reg2 = enc.reg(n.in(2)); + int body = Arm64.shift_reg(0b10011010110, reg2, op2, reg1, self); + enc.add4(body); + } + + // MUL can be considered an alias for MADD with the third operand Ra being set to 0 + + public static void madd(Encoding enc, Node n, int opcode, int ra) { + short self = enc.reg(n); + short reg1 = enc.reg(n.in(1)); + short reg2 = enc.reg(n.in(2)); + int body = Arm64.madd(opcode, reg2, ra, reg1, self); + enc.add4(body); + } + + public static void f_scalar(Encoding enc, Node n, int op ) { + short self = (short)(enc.reg(n) -D_OFFSET); + short reg1 = (short)(enc.reg(n.in(1))-D_OFFSET); + short reg2 = (short)(enc.reg(n.in(2))-D_OFFSET); + int body = Arm64.f_scalar(0b00011110, 1, reg2, op, reg1, self); + enc.add4(body); + } + + public static void patch_adrp_add(Encoding enc, int opStart, int delta, int rd) { + int target = opStart + delta; + int pageDelta = ((target & ~0xFFF) - (opStart & ~0xFFF)) >> 12; + int imm21 = pageDelta & 0x1FFFFF; + enc.patch4(opStart , Arm64.adrp(1, imm21 & 3, OP_ADRP, imm21 >>> 2, rd)); + enc.patch4(opStart+4, Arm64.imm_inst_l(OPI_ADD, target & 0xFFF, rd)); + } + + public static void f_cmp(Encoding enc, Node n) { + short reg1 = (short)(enc.reg(n.in(1))-D_OFFSET); + short reg2 = (short)(enc.reg(n.in(2))-D_OFFSET); + int body = Arm64.f_cmp(0b00011110, 3, reg2, reg1); + enc.add4(body); + } + + @Override public RegMask callArgMask(TypeFunPtr tfp, int idx, int maxArgSlot ) { return callInMask(tfp,idx,maxArgSlot); } + static RegMask callInMask(TypeFunPtr tfp, int idx, int maxArgSlot ) { + if( idx==0 ) return CodeGen.CODE._rpcMask; + if( idx==1 ) return null; + // Count floats in signature up to index + if( idx-2 >= tfp.nargs() ) return null; // Anti-dependence + boolean hidden = hiddenSelf(tfp); + int sigidx = idx-2; + if( hidden ) { + if( sigidx==0 ) return null; + sigidx--; + } + // Count floats in signature up to index + int fcnt=0; + for( int i=hidden ? 1 : 0; i0 ) throw Utils.TODO(); + return new RegMask(MAX_REG + 1 + sigidx); + } + + // Return the max stack slot used by this signature, or 0 + @Override public short maxArgSlot( TypeFunPtr tfp ) { + int icnt=0, fcnt=0; // Count of ints, floats + for( int i=hiddenSelf(tfp) ? 1 : 0; i 0 && tfp.arg(0) == TypePtr.PTR; + } + + private static final long CALLEE_SAVE = + 1L< add(add); + case AndNode and -> and(and); + case BoolNode bool -> cmp(bool); + case CallNode call -> call(call); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); + case CallEndNode cend-> new CallEndMach(cend); + case CLInitNode init -> new CLInitNode(init); + case CProjNode c -> new CProjNode(c); + case ConstantNode con-> con(con); + case DivNode div -> div.mode()==2 ? new DivFARM(div) : new DivARM(div); + case EscapeNode esc -> new EscapeNode(esc); + case FunNode fun -> new FunARM(fun); + case FunPtrNode fptr -> fptr(fptr); + case IfNode iff -> jmp(iff); + case LoadNode ld -> ld(ld); + case MemMergeNode mem-> new MemMergeNode(mem); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegARM(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFARM(mul) : new MulARM(mul); + case NewNode nnn -> new NewARM(nnn); + case NotNode not -> new NotARM(not); + case OrNode or -> or(or); + case ParmNode parm -> new ParmARM(parm); + case BulkMemPhiNode phi -> new BulkMemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); + case PhiNode phi -> new PhiNode(phi); + case ProjNode prj -> prj(prj); + case ReadOnlyNode read -> new ReadOnlyMach(read); + case ReturnNode ret -> new RetARM(ret,ret.fun()); + case SarNode sar -> asr(sar); + case ShlNode shl -> lsl(shl); + case ShrNode shr -> lsr(shr); + case StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); + case StartNode start -> new StartNode(start); + case StopNode stop -> new StopNode(stop); + case StoreNode st -> st(st); + case SubNode sub -> sub.mode()==2 ? new SubFARM(sub) : sub(sub); + case ToFloatNode tfn -> new I2F8ARM(tfn); + case ToIntegerNode cvt -> new F8ToIARM(cvt); + case XorNode xor -> xor(xor); + + case LoopNode loop -> new LoopNode(loop); + case RegionNode region-> new RegionNode(region); + default -> throw Utils.TODO(); + }; + } + + private Node cmp(BoolNode bool){ + Node cmp = _cmp(bool); + return new SetARM(cmp, bool.op(), cmp instanceof CmpFARM); + } + private Node _cmp(BoolNode bool) { + if( bool.isFloat() ) + return new CmpFARM(bool); + return bool.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti + ? new CmpIARM(bool, (int)ti.value()) + : new CmpARM(bool); + } + + private Node ld(LoadNode ld) { + return new LoadARM(address(ld), ld.ptr(), idx, off); + } + + private Node jmp(IfNode iff) { + // If/Bool combos will match to a Cmp/Set which sets flags. + // Most general arith ops will also set flags, which the Jmp needs directly. + // Loads do not set the flags, and will need an explicit TEST + String op = "!="; + boolean fp = false; + if( iff.in(1) instanceof BoolNode bool ) { op = bool.op(); fp = bool.isFloat(); } + else if( iff.in(1)==null ) op = "=="; // Never-node cutout + else iff.setDef(1, new BoolNode.NE(iff.in(1), + ConstantNode.raw(TypeInteger.ZERO), + (byte)(iff.in(1)._type instanceof TypeFloat ? 2 : 1))); + return new BranchARM(iff, op, fp); + } + + private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFARM(add); + return add.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && imm12(ti) + ? new AddIARM(add, (int)ti.value()) + : new AddARM(add); + } + + private Node sub(SubNode sub) { + return sub.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti) + ? new SubIARM(sub, (int)(ti.value())) + : new SubARM(sub); + } + + private Node con( ConstantNode con ) { + if( !con._con.isConstant() ) + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; + return switch( con._con ) { + case TypeInteger ti -> new IntARM(con,ext); + case TypeFloat tf -> new FltARM(con,ext); + case TypeMemPtr tmp -> new TMPARM(con,ext); + case TypeFunPtr tfp -> new TFPARM(tfp,ext); + case TypeNil tn -> throw Utils.TODO(); + // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. + case Type t -> t==Type.NIL ? new IntARM(con,ext) : ConstantNode.raw(con); + }; + } + + private Node fptr( FunPtrNode con ) { + return new TFPARM(con); + } + + private Node call(CallNode call){ + return call.fptr() instanceof FunPtrNode fptr + ? new CallARM(call, (TypeFunPtr)fptr._type) + : call.fptr() instanceof ConstantNode con + ? new CallARM(call, (TypeFunPtr)con._type) + : new CallRRARM(call); + } + + private Node or(OrNode or) { + int imm12; + return or.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && (imm12 = imm12Logical(ti)) != -1 + ? new OrIARM(or, imm12) + : new OrARM(or); + } + + private Node xor(XorNode xor) { + int imm12; + return xor.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && (imm12 = imm12Logical(ti)) != -1 + ? new XorIARM(xor, imm12) + : new XorARM(xor); + } + + private Node and(AndNode and) { + int imm12; + return and.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && (imm12 = imm12Logical(ti)) != -1 + ? new AndIARM(and, imm12) + : new AndARM(and); + } + + private Node asr(SarNode asr) { + return asr.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && ti.value() >= 0 && ti.value() < 63 + ? new AsrIARM(asr, (int)ti.value()) + : new AsrARM(asr); + } + + private Node lsl(ShlNode lsl) { + return lsl.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && ti.value() >= 0 && ti.value() < 63 + ? new LslIARM(lsl, (int)ti.value()) + : new LslARM(lsl); + } + + private Node lsr(ShrNode lsr) { + return lsr.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && ti.value() >= 0 && ti.value() < 63 + ? new LsrIARM(lsr, (int)ti.value()) + : new LsrARM(lsr); + } + + private Node prj(ProjNode prj) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); + TypeInteger ret = cReturn(prj); + if( ret!=null ) return new ExtendARM(prj,ret); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjARM(prj); + } + + private static int off; + private static Node idx; + private Node st(StoreNode st) { + return new StoreARM(address(st),st.ptr(),idx,off,st.val()); + } + + // Gather addressing mode bits prior to constructing. This is a builder + // pattern, but saving the bits in a *local* *global* here to keep mess + // contained. + private N address(N mop ) { + off = 0; // Reset + idx = null; + Node base = mop.ptr(); + // Skip/throw-away a ReadOnly, only used to typecheck + if( base instanceof ReadOnlyNode read ) base = read.in(1); + assert !(base instanceof AddNode) && base._type instanceof TypeMemPtr; // Base ptr always, not some derived + if( mop.off() instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm9(ti) ) { + off = (int)ti.value(); + assert off == ti.value(); // In 32-bit range + } else { + idx = mop.off(); + } + return mop; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java new file mode 100644 index 00000000..59bad64e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java @@ -0,0 +1,42 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.FunPtrNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Add upper 20bits to PC. Immediate comes from the relocation info. +public class AUIPC extends FunPtrNode implements MachNode, RIPRelSize { + AUIPC( FunPtrNode fptr ) { super(fptr); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public AUIPC copy() { return new AUIPC(this); } + @Override public String op() { return "auipc"; } + @Override public void encoding( Encoding enc ) { + enc.relo(this); + short dst = enc.reg(this); + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, dst, 0)); + } + + // Delta is from opcode start, but X86 measures from the end of the 5-byte encoding + @Override public byte encSize(int delta) { return 4; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + //short rpc = enc.reg(this); + //// High half is where the TFP constant used to be, the last input + //short auipc = enc.reg(in(_inputs._len-1)); + //enc.patch4(opStart,RiscV.i_type(0x67, rpc, 0, auipc, delta)); + throw Utils.TODO(); + } + + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + sb.p(reg).p(" = PC+#"); + _type.print(sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddFRISC.java new file mode 100644 index 00000000..d87e49b9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddFRISC.java @@ -0,0 +1,18 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fadd.d +public class AddFRISC extends MachConcreteNode implements MachNode { + AddFRISC(Node addf) {super(addf);} + @Override public String op() { return "addf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { riscv.rf_type(enc,this,RiscV.RM.RNE,1); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddIRISC.java new file mode 100644 index 00000000..8012b61e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddIRISC.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class AddIRISC extends ImmRISC { + // Used to inst-selection as a direct match against an ideal Add/Sub + public AddIRISC( Node add, int imm12, boolean pop ) { super(add,imm12,pop); } + @Override public String op() { return "addi"; } + @Override public String glabel() { return "+"; } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() {return 0;} + @Override public AddIRISC copy() { + // Clone the AddI, using the same inputs-only code used during inst select. + // Output edges are missing. + AddIRISC add = new AddIRISC(in(1),_imm12,false); + // Copys happen when output edges should be valid, so correct missing output edges. + in(1)._outputs.add(add); + return add; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddRISC.java new file mode 100644 index 00000000..7010916f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddRISC.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddRISC extends MachConcreteNode implements MachNode { + public AddRISC( Node add ) { super(add); } + AddRISC( Node base, Node off ) { + super(new Node[3]); + _inputs.set(1,base); + _inputs.set(2,off ); + } + @Override public String op() { return "add"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0,0); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndIRISC.java new file mode 100644 index 00000000..9fd65e7d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndIRISC.java @@ -0,0 +1,13 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class AndIRISC extends ImmRISC { + public AndIRISC( Node and, int imm) { super(and,imm); } + @Override public String op() { return "andi"; } + @Override public String glabel() { return "&"; } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() {return 7;} +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndRISC.java new file mode 100644 index 00000000..1845985e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndRISC.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AndRISC extends MachConcreteNode implements MachNode { + public AndRISC(Node and) { super(and); } + @Override public String op() { return "and"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0b111,0); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" & ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BranchRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BranchRISC.java new file mode 100644 index 00000000..a9d03da5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BranchRISC.java @@ -0,0 +1,95 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Conditional branch such as: BEQ +public class BranchRISC extends IfNode implements MachNode, RIPRelSize { + String _bop; + // label is obtained implicitly + public BranchRISC( IfNode iff, String bop, Node n1, Node n2 ) { + super(iff); + _bop = bop; + _inputs.setX(1,n1); + _inputs.setX(2,n2); + } + + @Override public String op() { return "b"+_bop; } + @Override public String label() { return op(); } + @Override public String comment() { return "L"+cproj(1)._nid; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return null; } + @Override public void negate() { + _bop = negate(_bop); + // Cannot encode ">" or "<=", so flip these + if( _bop.equals(">") || _bop.equals("<=") ) { + swap12(); // Cannot negate the test, so swap the operands + _bop = swap(_bop); // Swap test to match + } + } + + @Override protected ExprPrinter _print1(ExprPrinter p) { + p.p("if( "); + if( in(1)==null ) p.p("0"); + else p.n(in(1)); + p.p(_bop); + if( in(2)==null ) p.p("0"); + else p.n(in(2)); + return p.p(" )"); + } + + @Override public void encoding( Encoding enc ) { + if( in(1)==null && _bop=="!=" ) return; // Inverted never-node, no code + enc.jump(this,cproj(0)); + if( in(1)==null ) // Never node + enc.add4(RiscV.j_type(RiscV.OP_JAL, 0, 0)); + else { + // Todo: relocs (for offset - immf) + short src1 = enc.reg(in(1)); + short src2 = in(2)==null ? (short)riscv.ZERO : enc.reg(in(2)); + enc.add4(RiscV.b_type(RiscV.OP_BRANCH, RiscV.jumpop(_bop), src1, src2, 0)); + } + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( in(1)==null && _bop=="!=" ) return 0; // Inverted never-node, no code + if( -4*1024 <= delta && delta < 4*1024 ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + assert !( in(1)==null && _bop=="!=" ); // Inverted never-node, no code no patch + if( in(1)==null ) { // Never node + enc.patch4(opStart,RiscV.j_type(RiscV.OP_JAL, 0, delta)); + } else { + short src1 = enc.reg(in(1)); + short src2 = in(2)==null ? (short)riscv.ZERO : enc.reg(in(2)); + if( opLen==4 ) { + enc.patch4(opStart,RiscV.b_type(RiscV.OP_BRANCH, RiscV.jumpop(_bop), src1, src2, delta)); + } else { + throw Utils.TODO(); + } + } + } + + @Override public void asm(CodeGen code, SB sb) { + if( in(1)==null && _bop=="!=" ) { + sb.p("never"); + return; + } + String src1 = in(1)==null ? "#0" : code.reg(in(1)); + String src2 = in(2)==null ? "#0" : code.reg(in(2)); + sb.p(src1).p(" ").p(_bop).p(" ").p(src2).p(" "); + CFGNode prj = cproj(0).uctrlSkipEmpty(); + if( !prj.blockHead() ) prj = prj.cfg0(); + sb.p(label(prj)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java new file mode 100644 index 00000000..b1426a3f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class CallRISC extends CallNode implements MachNode, RIPRelSize { + final TypeFunPtr _tfp; + final String _name; + CallRISC( CallNode call, TypeFunPtr tfp ) { + super(call); + assert tfp.isConstant(); + _inputs.pop(); // Pop constant target + _tfp = tfp; + _name = CodeGen.CODE.funcName(tfp.fidx()); + } + + @Override public String op() { return "call"; } + @Override public String label() { return op(); } + @Override public String name() { return _name; } + @Override public TypeFunPtr tfp() { return _tfp; } + @Override public RegMask regmap(int i) { return riscv.callInMask(_tfp,i,fun()._maxArgSlot); } + @Override public RegMask outregmap() { return riscv.RPC_MASK; } + @Override public int nargs() { return nIns()-2; } // Minus control, memory, fptr + + @Override public void encoding( Encoding enc ) { + // Short form +/-4K: beq r0,r0,imm12 + // Long form: auipc rX,imm20/32; jal r0,[rX+imm12/32] + FunNode fun = CodeGen.CODE.link(_tfp); + if( fun==null ) enc.external(this,_name); + else enc.relo(this); + short rpc = enc.reg(this); + enc.add4(RiscV.j_type(RiscV.OP_JAL, rpc, 0)); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( -(1L<<20) <= delta && delta < (1L<<20) ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short rpc = enc.reg(this); + if( opLen==4 ) { + enc.patch4(opStart,RiscV.j_type(RiscV.OP_JAL, rpc, delta)); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(_name).p(" "); + for( int i=0; i>12)); + // Load dst,[dst+#low12_constant_pool] + enc.patch4(opStart+4, RiscV.i_type(RiscV.OP_LOADFP, dst, 0b11, tmp, delta & 0xFFF)); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" = #")); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FunRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FunRISC.java new file mode 100644 index 00000000..d3d64b06 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FunRISC.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class FunRISC extends FunNode implements MachNode { + FunRISC(FunNode fun) {super(fun);} + @Override public void computeFrameAdjust(CodeGen code, int maxReg) { + super.computeFrameAdjust(code,maxReg); + if( _hasCalls ) // If non-leaf, pad to 16b + _frameAdjust = ((_frameAdjust+8) & -16); + } + @Override public void encoding( Encoding enc ) { + int sz = _frameAdjust; + if( sz == 0 ) return; // Skip if no frame adjust + if( sz >= 1L<<12 ) throw Utils.TODO(); + enc.add4(RiscV.i_type(RiscV.OP_IMM, riscv.RSP, 0, riscv.RSP, -sz & 0xFFF)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/I2F8RISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/I2F8RISC.java new file mode 100644 index 00000000..1b53ab0f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/I2F8RISC.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fcvt.d.w +// Converts a 32-bit signed integer, in integer register rs1 into a double-precision floating-point number in floating-point register rd. +public class I2F8RISC extends MachConcreteNode implements MachNode { + I2F8RISC(Node i2f8) {super(i2f8);} + @Override public String op() { return "cvtf"; } + @Override public RegMask regmap(int i) { assert i==1; return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { + short dst = (short)(enc.reg(this )-riscv.F_OFFSET); + short src1 = enc.reg(in(1)); + int body = RiscV.r_type(RiscV.OP_FP,dst,RiscV.RM.RNE.ordinal(),src1,0,0x69); + enc.add4(body); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p("(flt)").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ImmRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ImmRISC.java new file mode 100644 index 00000000..ee247c3e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ImmRISC.java @@ -0,0 +1,55 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +abstract public class ImmRISC extends MachConcreteNode implements MachNode { + public final int _imm12; + ImmRISC( Node add, int imm12, boolean pop ) { + super(add); + _imm12 = imm12; + // If pop, we are moving from an ideal Node to MachNode; the ideal Node + // inputs got copied here, but we are absorbing the immediate into the + // MachNode and so do not want the constant input. + if( pop ) _inputs.pop(); + // if NOT pop, we are the 2nd-half of a 2-part expansion, reset the + // input edges to no-control + else { _inputs.set(0,null); _inputs.setX(1,add); } + } + ImmRISC( Node add, int imm12 ) { this(add,imm12,true); } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { + // Allow cloning of the 2nd half of a 2-part constant; this might + // trigger cloning the first part also. + return in(1) instanceof LUI || in(1) instanceof AUIPC; + } + + @Override protected ExprPrinter _print1(ExprPrinter p) { + int imm12 = (_imm12<<20)>>20; // Sign extend 12 bits + p.p("( ").n(in(1)); + return p.p(String.format(" %s #%d )",glabel(),imm12)); + } + + abstract int opcode(); + abstract int func3(); + + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + enc.add4(RiscV.i_type(opcode(), dst, func3(), src, _imm12 & 0xFFF)); + } + + // General form: "addi rd = rs1 + imm" + @Override public void asm(CodeGen code, SB sb) { + int imm12 = (_imm12<<20)>>20; // Sign extend 12 bits + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ").p(glabel()).p(" #").p(imm12); + if( in(1) instanceof LUI lui ) + sb.p(" // #").hex4((int)(((TypeInteger)lui._con).value()) + imm12); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java new file mode 100644 index 00000000..dd4a02a3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Special instruction for loading 8 byte constants from the constant pool. + +public class Int8RISC extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + Int8RISC( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ld8"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public Int8RISC copy() { return new Int8RISC(this,_ext); } + + @Override public byte encSize(int delta) { return 8; } + + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); + short dst = enc.reg(this); + short tmp = (short)riscv.T6; + enc.largeConstant(this,_con, 0, -1); + // AUIPC dst,#hi20_constant_pool + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, tmp, 0)); + // Load dst,[dst+#low12_constant_pool] + enc.add4(RiscV.i_type(RiscV.OP_LOAD, dst, 0b11, tmp, 0)); + } + @Override public RegMask killmap() { return new RegMask(riscv.T6); } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short dst = enc.reg(this); + short tmp = (short)riscv.T6; + // AUIPC dst,#hi20_constant_pool + enc.patch4(opStart , RiscV.u_type(RiscV.OP_AUIPC, tmp, delta>>12)); + // Load dst,[dst+#low12_constant_pool] + enc.patch4(opStart+4, RiscV.i_type(RiscV.OP_LOAD, dst, 0b11, tmp, delta & 0xFFF)); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" = #")); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java new file mode 100644 index 00000000..35d2bc65 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java @@ -0,0 +1,31 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +// 12-bit integer constant. Larger constants are made up in the instruction +// selection by adding with a LUI. +public class IntRISC extends ConstantNode implements MachNode { + IntRISC(ConstantNode con) { super(con); } + @Override public String op() { return "ldi"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public IntRISC copy() { return new IntRISC(this); } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this); + int val = _con==Type.NIL ? 0 : (int)(((TypeInteger)_con).value() & 0xFFF); + // Explicit truncation of larger immediates; this will sign-extend on + // load and this is handled during instruction selection. + enc.add4(RiscV.i_type(RiscV.OP_IMM, dst, 0, riscv.ZERO, val)); + } + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + _con.print(sb.p(reg).p(" = #")); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LUI.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LUI.java new file mode 100644 index 00000000..aedd8b47 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LUI.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +// Load upper 20bits. +public class LUI extends ConstantNode implements MachNode { + public LUI( int imm20 ) { + super(TypeInteger.constant(imm20)); + assert (imm20 & 0xFFF)==0; + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public LUI copy() { return new LUI((int)((TypeInteger)_con).value()); } + @Override public String op() { return "lui"; } + @Override public void encoding( Encoding enc ) { + long x = ((TypeInteger)_con).value(); + int imm20 = (int)(x>>12) & 0xFFFFF; + short dst = enc.reg(this); + int lui = RiscV.u_type(RiscV.OP_LUI, dst, imm20); + enc.add4(lui); + } + + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + sb.p(reg).p(" = #").hex4((int)(((TypeInteger)_con).value())); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java new file mode 100644 index 00000000..e9f3ec02 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Load memory addressing on RISC +// Support imm, reg(direct), or reg+off(indirect) addressing +// Base = base - base pointer, offset is added to base +// idx = null +// off = off - imm12 added to base +public class LoadRISC extends MemOpRISC { + public LoadRISC(LoadNode ld, Node base, int off) { super(ld, base, off, null); } + @Override public String op() { return "ld" +_sz; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.MEM_MASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short ptr = enc.reg(ptr()); + int op = dst >= riscv.F_OFFSET ? RiscV.OP_LOADFP : RiscV.OP_LOAD; + if( dst >= riscv.F_OFFSET ) dst -= riscv.F_OFFSET; + enc.add4(RiscV.i_type(op, dst, func3(), ptr, _off)); + } + @Override int func3() { + int func3 = -1; + // no unsigned flavour for store, so both signed and unsigned trigger the same + Type declType = _con; + if( declType == TypeInteger. I8 ) func3=0; // LB + if( declType == TypeInteger.I16 ) func3=1; // LH + if( declType == TypeInteger.I32 ) func3=2; // LW + if( declType == TypeInteger.BOT ) func3=3; // LD + if( declType == TypeInteger. U8 ) func3=4; // LBU + if( declType == TypeInteger.BOOL) func3=4; // LBU + if( declType == TypeInteger.U16 ) func3=5; // LHU + if( declType == TypeInteger.U32 ) func3=6; // LWU + if( declType instanceof TypeInteger ti ) { + if( -128 <= ti._min && ti._max < 128 ) func3 = 0; // LB + } + + // float + if( declType == TypeFloat.F32) func3 = 2; // fLW fSW + if( declType == TypeFloat.F64) func3 = 3; // fLD fSD + + if( declType instanceof TypeMemPtr) func3 = 3; // 8 byte pointers (pick ld) + if( declType instanceof TypeFunPtr) func3 = 3; // 8 byte function pointers + if( declType instanceof TypeConAry ary ) + func3 = switch( ary.log_size() ) { + case 0 -> 4; // LBU + case 1 -> 5; // LHU + case 2 -> 2; // LW + case 3 -> 3; // LD + default -> throw Utils.TODO("RISC-V constant-array load for "+declType); + }; + if( func3 == -1 ) throw Utils.TODO("RISC-V load for "+declType); + return func3; + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(","); + asm_address(code,sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java new file mode 100644 index 00000000..e0bbc219 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java @@ -0,0 +1,51 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +public abstract class MemOpRISC extends MemOpNode implements MachNode { + final int _off; // Limit 12 bits + final char _sz; + final int _bytes; + MemOpRISC(MemOpNode mop, Node base, int off, Node val) { + super(mop,mop); + assert base._type instanceof TypeMemPtr; + _inputs.setX(2, base ); // Base can be an Add, is no longer raw object base + _inputs.setX(3, null); // Never an index + _inputs.setX(4, val ); + _off = off; + _bytes = mop instanceof StoreNode st ? st.storeSize() : 1<<_con.log_size(); + _sz = (char)('0'+_bytes); + } + + @Override public String label() { return op(); } + Node val() { return in(4); } // Only for stores + + @Override protected ExprPrinter _printMach(ExprPrinter p) { return p.p(".").p(_name); } + + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } + + // func3 is based on load/store size and extend + abstract int func3(); + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { + // 0 - ctrl + // 1 - memory + if( i==2 ) return riscv.RMASK; // base + // 2 - index + if( i==4 ) return riscv.MEM_MASK; // value + throw Utils.TODO(); + } + + SB asm_address(CodeGen code, SB sb) { + sb.p("[").p(code.reg(ptr())).p("+"); + return sb.p(_off).p("]"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulFRISC.java new file mode 100644 index 00000000..dacec2b5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulFRISC.java @@ -0,0 +1,18 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fmul.d +public class MulFRISC extends MachConcreteNode implements MachNode { + MulFRISC(Node mulf) {super(mulf);} + @Override public String op() { return "mulf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { riscv.rf_type(enc,this,RiscV.RM.RNE,9); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulRISC.java new file mode 100644 index 00000000..9cb9ace7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// mulh signed multiply instruction(no-imm form) +public class MulRISC extends MachConcreteNode implements MachNode{ + public MulRISC(Node mul) {super(mul);} + @Override public String op() { return "mul"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0,1); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NegRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NegRISC.java new file mode 100644 index 00000000..3070c56c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NegRISC.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class NegRISC extends MachConcreteNode implements MachNode { + public NegRISC( Node neg ) { super(neg); } + @Override public String op() { return "neg"; } + @Override public RegMask regmap(int i) { assert i==1; return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + RiscV.r_type(RiscV.OP,dst,0,0,src,0x20); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = -").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java new file mode 100644 index 00000000..40d0359c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.print.ASMPrinter; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class NewRISC extends NewNode implements MachNode, RIPRelSize { + // A pre-zeroed chunk of memory. + NewRISC( NewNode nnn ) { super(nnn); } + @Override public void encoding( Encoding enc ) { + // Generic external encoding; 2 ops. + enc.external(this,"calloc"); + // A1 is a caller-save, allowed to crush building external address + // auipc + enc.add4(RiscV.i_type(RiscV.OP_IMM , _arg2Reg , 0, riscv.ZERO, 1)); + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, riscv.A2 , 0)); + enc.add4(RiscV.i_type(RiscV.OP_JALR , riscv.RPC, 0, riscv.A2, 0)); + } + + // Patch is for running "new" in a JIT. + // Delta is from opcode start + @Override public byte encSize(int delta) { return 4*3; } + + // Patch is for running "new" in a JIT. + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + // Negative patches are JIT emulator targets. + if( opStart+delta < 0 ) { + delta -= 4; // Offset from the AUIPC start + int imm20 = delta>>12; + if( ((delta>>11)&1)==1 ) imm20++; // Correct accidental sign extension + int imm12 = delta&0xFFF; + enc.patch4(opStart+4,RiscV.u_type(RiscV.OP_AUIPC, riscv.A2 , imm20)); + enc.patch4(opStart+8,RiscV.i_type(RiscV.OP_JALR , riscv.RPC, 0, riscv.A2, imm12)); + } else + throw Utils.TODO(); + } + + // General form: "alloc #bytes PC" + @Override public void asm(CodeGen code, SB sb) { + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); + sb.p("auipc a2=#calloc\n"); + sb.p("call a2+#calloc"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NotRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NotRISC.java new file mode 100644 index 00000000..6f499db1 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NotRISC.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.NotNode; +import com.seaofnodes.simple.util.SB; + +public class NotRISC extends MachConcreteNode implements MachNode { + public NotRISC(NotNode not) { super(not); } + @Override public String op() { return "not"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + // SEQZ is unsigned: negative nonzero inputs must also produce false. + enc.add4(RiscV.i_type(RiscV.OP_IMM, dst, 3, src, 1)); + } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrIRISC.java new file mode 100644 index 00000000..937595a3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrIRISC.java @@ -0,0 +1,13 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class OrIRISC extends ImmRISC { + public OrIRISC( Node and, int imm) { super(and,imm); } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 6; } + @Override public String glabel() { return "|"; } + @Override public String op() { return "ori"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrRISC.java new file mode 100644 index 00000000..a310a1f4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrRISC.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class OrRISC extends MachConcreteNode implements MachNode { + public OrRISC(Node or) { super(or); } + @Override public String op() { return "or"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,6,0); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" | ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ParmRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ParmRISC.java new file mode 100644 index 00000000..0f8db4e5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ParmRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ParmNode; + +public class ParmRISC extends ParmNode implements MachNode { + final RegMask _rmask; + ParmRISC(ParmNode parm) { + super(parm); + _rmask = riscv.callInMask(fun().sig(),_idx,0); + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return _rmask; } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ProjRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ProjRISC.java new file mode 100644 index 00000000..2f9ac469 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ProjRISC.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ProjNode; + +public class ProjRISC extends ProjNode implements MachNode { + ProjRISC(ProjNode p) { super(p); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { + return ((MachNode)in(0)).outregmap(_idx); + } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/RetRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/RetRISC.java new file mode 100644 index 00000000..33143c6b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/RetRISC.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ReturnNode; +import com.seaofnodes.simple.util.Utils; + +public class RetRISC extends ReturnNode implements MachNode { + public RetRISC(ReturnNode ret, FunNode fun) { super(ret, fun); fun.setRet(this); } + @Override public void encoding( Encoding enc ) { + int sz = fun()._frameAdjust; + if( sz >= 1L<<12 ) throw Utils.TODO(); + if( sz != 0 ) + enc.add4(RiscV.i_type(RiscV.OP_IMM, riscv.RSP, 0, riscv.RSP, sz & 0xFFF)); + short rpc = enc.reg(rpc()); + enc.add4(RiscV.i_type(RiscV.OP_JALR, riscv.ZERO, 0, rpc, 0)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetFRISC.java new file mode 100644 index 00000000..d037dd5c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetFRISC.java @@ -0,0 +1,34 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.BoolNode; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; + +// corresponds to feq.d = equal +// fle.d = less than or equal +// flt.d = less than +public class SetFRISC extends MachConcreteNode implements MachNode { + final String _bop; // One of <,<=,== + SetFRISC( BoolNode bool ) { + super(bool); + assert bool.isFloat(); + _bop = bool.op(); + } + @Override public String op() { return "set"+_bop; } + @Override public RegMask regmap(int i) { return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src1 = (short)(enc.reg(in(1))-riscv.F_OFFSET); + short src2 = (short)(enc.reg(in(2))-riscv.F_OFFSET); + int body = RiscV.r_type(RiscV.OP_FP,dst,RiscV.fsetop(_bop),src1,src2,0b1010001/*FCMP*/); + enc.add4(body); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ").p(_bop).p(" ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetIRISC.java new file mode 100644 index 00000000..9bc86565 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetIRISC.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.node.Node; + +// corresponds to slti,sltiu +public class SetIRISC extends ImmRISC { + final boolean _unsigned; // slti vs sltiu + public SetIRISC( Node src, int imm12, boolean unsigned ) { + this(src,imm12,unsigned,true); + } + public SetIRISC( Node src, int imm12, boolean unsigned, boolean pop ) { + super(src,imm12,pop); + if( !pop ) _inputs.setLen(2); + _unsigned = unsigned; + } + @Override public String op() { return "slt" + (_unsigned ? "u":"") + "i"; } + @Override public String glabel() { return "<"+ (_unsigned ? "u":""); } + @Override int opcode() { return 0b0010011; } + @Override int func3() { return _unsigned ? 0b011 : 0b010; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetRISC.java new file mode 100644 index 00000000..848cc59f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetRISC.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// corresponds to slt sltu +public class SetRISC extends MachConcreteNode implements MachNode { + final boolean _unsigned; + public SetRISC( Node cmp, boolean unsigned ) { super(cmp); _unsigned=unsigned; } + @Override public String op() { return "slt" + (_unsigned ? "u":""); } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + riscv.r_type(enc,this,_unsigned ? 0b011 : 0b010,0); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" < ").p(code.reg(in(2))); + } +} \ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllIRISC.java new file mode 100644 index 00000000..1425caa2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllIRISC.java @@ -0,0 +1,13 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class SllIRISC extends ImmRISC { + public SllIRISC( Node and, int imm) { super(and,imm); } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 1; } + @Override public String glabel() { return "<<"; } + @Override public String op() { return "slli"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllRISC.java new file mode 100644 index 00000000..5a4d4cc6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Shift left logical +public class SllRISC extends MachConcreteNode implements MachNode { + public SllRISC(Node sll) { super(sll); } + @Override public String op() { return "shl"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,1,0); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" << ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java new file mode 100644 index 00000000..d455337b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java @@ -0,0 +1,61 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.SplitNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class SplitRISC extends SplitNode { + SplitRISC( LRG lrg, String kind, byte round ) { super(lrg, kind,round, new Node[2]); } + @Override public RegMask regmap(int i) { return riscv.SPLIT_MASK; } + @Override public RegMask outregmap() { return riscv.SPLIT_MASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + + // Flag for being either XMM or stack + boolean dstX = dst >= riscv.F_OFFSET; + boolean srcX = src >= riscv.F_OFFSET; + + if( dst >= riscv.MAX_REG ) { + // Store to SP + if( src >= riscv.MAX_REG ) { + throw Utils.TODO(); // Very rare stack-stack move + } + int off = fun(enc).computeStackOffset(enc._code,dst); + int op = srcX ? RiscV.OP_STOREFP : RiscV.OP_STORE; + if( srcX ) src -= riscv.F_OFFSET; + enc.add4(RiscV.s_type(op, 0b011, riscv.RSP, src, off)); + return; + } + if( src >= riscv.MAX_REG ) { + // Load from SP + int off = fun(enc).computeStackOffset(enc._code,src); + int op = dstX ? RiscV.OP_LOADFP : RiscV.OP_LOAD; + if( dstX ) dst -= riscv.F_OFFSET; + enc.add4(RiscV.i_type(op, dst, 0b011, riscv.RSP, off)); + return; + } + // pick opcode based on regs + if( !dstX && !srcX ) { + // GPR->GPR + enc.add4(RiscV.r_type(RiscV.OP,dst,0,src,riscv.ZERO,0)); + } else if( dstX && srcX ) { + // FPR->FPR + src -= riscv.F_OFFSET; + dst -= riscv.F_OFFSET; + enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0b000, src, src, 0b0010101)); // dst = FMIN(src,src) + } else if(!srcX && dstX) { + //GPR->FPR + // fmv.d.x + enc.add4(RiscV.r_type(RiscV.OP_FP, dst - riscv.F_OFFSET, 0, src, 0, 0b1111001)); + } else if(srcX && !dstX) { + //FPR->GPR + //fmv.x.d + enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0, src - riscv.F_OFFSET, 0, 0b1110001)); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraIRISC.java new file mode 100644 index 00000000..b316b303 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraIRISC.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class SraIRISC extends ImmRISC { + public SraIRISC( Node and, int imm) { + super(and,imm | (0x20 << 5)); + } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 5;} + @Override public String glabel() { return ">>"; } + @Override public String op() { return "srai"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraRISC.java new file mode 100644 index 00000000..e2e34415 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Right Shift Arithmetic +public class SraRISC extends MachConcreteNode implements MachNode { + public SraRISC(Node sra) { super(sra); } + @Override public String op() { return "sar"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0b101,0b00100000); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >> ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlIRISC.java new file mode 100644 index 00000000..d0ea6b18 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlIRISC.java @@ -0,0 +1,13 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class SrlIRISC extends ImmRISC { + public SrlIRISC( Node and, int imm, boolean pop) { super(and,imm,pop); } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 5; } + @Override public String glabel() { return ">>>"; } + @Override public String op() { return "srli"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlRISC.java new file mode 100644 index 00000000..1eccc1c8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Right Shift Logical +public class SrlRISC extends MachConcreteNode implements MachNode { + public SrlRISC(Node srli) { super(srli); } + @Override public String op() { return "shr"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,7,0); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >>> ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java new file mode 100644 index 00000000..c3fcf4d3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java @@ -0,0 +1,56 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Store memory addressing on ARM +// Support imm, reg(direct), or reg+off(indirect) addressing +// Base = base - base pointer, offset is added to base +// idx = null +// off = off - offset added to base) +// imm = imm - immediate value to store +// val = Node of immediate value to store(null if its a constant immediate) + +//e.g s.cs[0] = 67; // C +// base = s.cs, off = 4, imm = 67, val = null + +// sw rs2,offset(rs1) +public class StoreRISC extends MemOpRISC { + StoreRISC( StoreNode st, Node base, int off, Node val ) { super(st, base, off, val); } + @Override public String op() { return "st"+_sz; } + @Override public RegMask regmap(int i) { + // 0 - ctrl + if( i==1 ) return null; // mem + if( i==2 ) return riscv.RMASK; // ptr + // 2 - index + if( i==4 ) return _bytes >= 4 + ? riscv.MEM_MASK // Word/dword value in GPR or FPR + : riscv.RMASK; // Byte/short stores require a GPR + return null; // Anti-dependence + } + @Override public RegMask outregmap() { return null; } + + @Override public void encoding( Encoding enc ) { + short val = enc.reg(val()); + short ptr = enc.reg(ptr()); + int op = val >= riscv.F_OFFSET ? RiscV.OP_STOREFP : RiscV.OP_STORE; + if( val >= riscv.F_OFFSET ) val -= riscv.F_OFFSET; + enc.add4(RiscV.s_type(op, func3()&7, ptr, val == -1 ? riscv.ZERO : val, _off)); + } + + // func3 is based on load/store size and extend + @Override int func3() { + return Integer.numberOfTrailingZeros(_bytes); + } + + @Override public void asm(CodeGen code, SB sb) { + asm_address(code,sb).p(","); + if( val()==null ) sb.p("#").p("0"); + else sb.p(code.reg(val())); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubFRISC.java new file mode 100644 index 00000000..044820cf --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubFRISC.java @@ -0,0 +1,18 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fsub.d +public class SubFRISC extends MachConcreteNode implements MachNode { + SubFRISC(Node subf) {super(subf);} + @Override public String op() { return "subf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { riscv.rf_type(enc,this,RiscV.RM.RNE,5); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubRISC.java new file mode 100644 index 00000000..2adfce23 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubRISC.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class SubRISC extends MachConcreteNode implements MachNode { + public SubRISC( Node sub ) { super(sub); } + @Override public String op() { return "sub"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0,0x20); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" - ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java new file mode 100644 index 00000000..44ef1a95 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java @@ -0,0 +1,56 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class TFPRISC extends FunPtrNode implements MachNode, RIPRelSize { + final String _ext; + // Pointer to a Simple function + TFPRISC( FunPtrNode fptr ) { super(fptr); _ext = null; } + // Pointer to an Extern "C" function + TFPRISC( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } + @Override public String op() { return "ldx"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public TFPRISC copy() { return new TFPRISC((TypeFunPtr)_con,_ext); } + @Override public void encoding( Encoding enc ) { + if( _ext==null ) enc.relo(this); // Internal patch + else enc.external(this,_ext); // ELF-file external patch + // TODO: 1 op encoding, plus a TODO if it does not fit + short dst = enc.reg(this); + // auipc t0,0 + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, dst, 0)); + // addi t1,t0 + #0 + enc.add4(RiscV.i_type(RiscV.OP_IMM, dst, 0, dst, 0)); + } + + @Override public byte encSize(int delta) { return 8; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short rpc = enc.reg(this); + if( opLen==8 ) { + int hi20 = (delta + 0x800) >> 12; + int lo12 = delta - (hi20 << 12); + // AUIPC (upper 20 bits) + // opstart of add + enc.patch4(opStart ,RiscV.u_type(RiscV.OP_AUIPC, rpc, hi20)); + // addi(low 12 bits) + enc.patch4(opStart+4,RiscV.i_type(RiscV.OP_IMM, rpc, 0, rpc, lo12 & 0xFFF)); + // addi + } else { + // should not happen as one instruction is 4 byte, and TFP arm encodes 2. + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + _type.print(sb.p(code.reg(this)).p(" #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java new file mode 100644 index 00000000..a5ff3753 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.util.SB; + +public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + TMPRISC( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ldp"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public TMPRISC copy() { return new TMPRISC(this,_ext); } + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1); + short dst = enc.reg(this); + // AUIPC dst,#hi20_constant_pool + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, dst, 0)); + // addi dst,[dst+#low12_constant_pool] + enc.add4(RiscV.i_type(RiscV.OP_IMM, dst, 0, dst, 0)); + } + + @Override public byte encSize(int delta) { return 8; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short dst = enc.reg(this); + // AUIPC dst,#hi20_constant_pool + enc.patch4(opStart , RiscV.u_type(RiscV.OP_AUIPC, dst, (delta+0x800)>>12)); + // ADDI's low 12 bits are signed; round the AUIPC page accordingly. + enc.patch4(opStart+4, RiscV.i_type(RiscV.OP_IMM, dst, 0, dst, delta & 0xFFF)); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/UJmpRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/UJmpRISC.java new file mode 100644 index 00000000..448d9ec3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/UJmpRISC.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// unconditional jump +public class UJmpRISC extends CFGNode implements MachNode, RIPRelSize { + @Override public String op() { return "jmp"; } + @Override public String label() { return op(); } + @Override protected ExprPrinter _print1(ExprPrinter p) { return p.p("jmp "); } + @Override public RegMask regmap(int i) {return null; } + @Override public RegMask outregmap() { return null; } + @Override public Type compute() { throw Utils.TODO(); } + @Override public Node idealize() { throw Utils.TODO(); } + @Override public void encoding( Encoding enc ) { + // Short form +/-4K: beq r0,r0,imm12 + // Long form: auipc rX,imm20/32; jal r0,[rX+imm12/32] + enc.jump(this,uctrl()); + enc.add4(RiscV.j_type(RiscV.OP_JAL, 0, 0)); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( -(1L<<20) <= delta && delta < (1L<<20) ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + if( opLen==4 ) { + enc.patch4(opStart,RiscV.j_type(RiscV.OP_JAL, 0, delta)); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + CFGNode target = uctrl(); + //assert target.nOuts() > 1; // Should optimize jmp to empty targets + sb.p(label(target)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorIRISC.java new file mode 100644 index 00000000..f4323cae --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorIRISC.java @@ -0,0 +1,14 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class XorIRISC extends ImmRISC { + public XorIRISC( Node and, int imm) { super(and,imm); } + public XorIRISC( Node and, int imm, boolean pop) { super(and,imm,pop); } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 4; } + @Override public String glabel() { return "^"; } + @Override public String op() { return "xori"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorRISC.java new file mode 100644 index 00000000..413a7c3b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorRISC.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class XorRISC extends MachConcreteNode implements MachNode { + public XorRISC(Node or) { super(or); } + @Override public String op() { return "xor"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,4,0); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ^ ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java new file mode 100644 index 00000000..0195542c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java @@ -0,0 +1,450 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import static com.seaofnodes.isa.RiscV.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class riscv extends Machine { + + public riscv(CodeGen code) { + if( !"SystemV".equals(code._callingConv) ) + throw new IllegalArgumentException("Unknown calling convention "+code._callingConv); + } + + @Override public String name() {return "riscv";} + + // Using ABI names instead of register names + public static int ZERO = 0, RPC= 1, RSP= 2, S12= 3, S13= 4, T0 = 5, T1 = 6, T2 = 7; + public static int S0 = 8, S1 = 9, A0 = 10, A1 = 11, A2 = 12, A3 = 13, A4 = 14, A5 = 15; + public static int A6 = 16, A7 = 17, S2 = 18, S3 = 19, S4 = 20, S5 = 21, S6 = 22, S7 = 23; + public static int S8 = 24, S9 = 25, S10= 26, S11= 27, T3 = 28, T4 = 29, T5 = 30, T6 = 31; + + // FP registers + public static int F0 = 32, F1 = 33, F2 = 34, F3 = 35, F4 = 36, F5 = 37, F6 = 38, F7 = 39; + public static int FS0 = 40, FS1 = 41, FA0 = 42, FA1 = 43, FA2 = 44, FA3 = 45, FA4 = 46, FA5 = 47; + public static int FA6 = 48, FA7 = 49, FS2 = 50, FS3 = 51, FS4 = 52, FS5 = 53, FS6 = 54, FS7 = 55; + public static int FS8 = 56, FS9 = 57, FS10 = 58, FS11 = 59, FT8 = 60, FT9 = 61, FT10 = 62, FT11 = 63; + + static final int MAX_REG = 64; + + public static final int F_OFFSET = 32; + + static final String[] REGS = new String[] { + //"zero","rpc" , "rsp" , "s12" , "s13" , "t0" , "t1" , "t2" , + //"s0" , "s1" , "a0" , "a1" , "a2" , "a3" , "a4" , "a5" , + //"a6" , "a7" , "s2" , "s3" , "s4" , "s5" , "s6" , "s7" , + //"s8" , "s9" , "s10" , "s11" , "t3" , "t4" , "t5" , "t6" , + //"f0" , "f1" , "f2" , "f3" , "f4" , "f5" , "f6" , "f7" , + //"fs0" , "fs1" , "fa0" , "fa1" , "fa2" , "fa3" , "fa4" , "fa5" , + //"fa6" , "fa7" , "fs2" , "fs3" , "fs4" , "fs5" , "fs6" , "fs7" , + //"fs8" , "fs9" , "fs10", "fs11", "ft8" , "ft9" , "ft10", "ft11" + // Register-based naming hella easier to debug than "official" register + // names when single-stepping in debugger. + "zero", "r1" , "rsp" , "r3" , "r4" , "r5" , "r6" , "r7" , + "r8" , "r9" , "r10" , "r11", "r12", "r13", "r14", "r15", + "r16" , "r17" , "r18" , "r19", "r20", "r21", "r22", "r23", + "r24" , "r25" , "r26" , "r27", "r28", "r29", "r30", "r31", + "f0" , "f1" , "f2" , "f3" , "f4" , "f5" , "f6" , "f7" , + "fs0" , "fs1" , "fa0" , "fa1" , "fa2" , "fa3" , "fa4" , "fa5" , + "fa6" , "fa7" , "fs2" , "fs3" , "fs4" , "fs5" , "fs6" , "fs7" , + "fs8" , "fs9" , "fs10", "fs11", "ft8" , "ft9" , "ft10", "ft11" + }; + @Override public String[] regs() { return REGS; } + + // General purpose register mask: pointers and ints, not floats + static final long RD_BITS = 0b11111111111111111111111111111111L; // All the GPRs + static final RegMask RMASK = new RegMask(RD_BITS); + + static final long WR_BITS = 0b11111111111111111111111111111010L; // All the GPRs, minus ZERO and RSP + static final RegMask WMASK = new RegMask(WR_BITS); + // Float mask from(f0-ft10). TODO: ft10,ft11 needs a larger RegMask + static final long FP_BITS = 0b11111111111111111111111111111111L<= tfp.nargs() ) return null; // Anti-dependence + boolean hidden = hiddenSelf(tfp); + int sigidx = idx-2; + if( hidden ) { + if( sigidx==0 ) return null; + sigidx--; + } + // Count floats in signature up to index + int fcnt=0; + for( int i=hidden ? 1 : 0; i0 ) throw Utils.TODO(); + return new RegMask(MAX_REG + 1 + sigidx); + } + + @Override public short maxArgSlot( TypeFunPtr tfp ) { + int icnt=0, fcnt=0; // Count of ints, floats + for( int i=hiddenSelf(tfp) ? 1 : 0; i 0 && tfp.arg(0) == TypePtr.PTR; + } + + static final long CALLER_SAVE = + (1L<>52) == ti.value(); + } + // True if HIGH 20-bit signed immediate, with all zeros low. + public static boolean imm20Exact(TypeInteger ti) { + // shift left 32 to clear out the upper 32 bits. + // shift right SIGNED to sign-extend upper 32 bits; then shift 12 more to clear out lower 12 bits. + // shift left 12 to re-center the bits. + return ti.isConstant() && (((ti.value()<<32)>>44)<<12) == ti.value(); + } + + @Override public Node instSelect( Node n ) { + return switch (n) { + case AddNode add -> add(add); + case AndNode and -> and(and); + case BoolNode bool -> cmp(bool); + case CallNode call -> call(call); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); + case CallEndNode cend -> new CallEndMach(cend); + case CLInitNode init -> new CLInitNode(init); + case CProjNode c -> new CProjNode(c); + case ConstantNode con -> con(con); + case DivNode div -> div.mode()==2 ? new DivFRISC(div) : new DivRISC(div); + case EscapeNode esc -> new EscapeNode(esc); + case FunNode fun -> new FunRISC(fun); + case FunPtrNode fptr -> fptr(fptr); + case IfNode iff -> jmp(iff); + case LoadNode ld -> ld(ld); + case MemMergeNode mem -> new MemMergeNode(mem); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegRISC(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFRISC(mul) : new MulRISC(mul); + case NewNode nnn -> nnn(nnn); + case NotNode not -> new NotRISC(not); + case OrNode or -> or(or); + case ParmNode parm -> new ParmRISC(parm); + case BulkMemPhiNode phi-> new BulkMemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); + case PhiNode phi -> new PhiNode(phi); + case ProjNode prj -> prj(prj); + case ReadOnlyNode read-> new ReadOnlyMach(read); + case ReturnNode ret -> new RetRISC(ret, ret.fun()); + case SarNode sar -> sra(sar); + case ShlNode shl -> sll(shl); + case ShrNode shr -> srl(shr); + case StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); + case StartNode start -> new StartNode(start); + case StopNode stop -> new StopNode(stop); + case StoreNode st -> st(st); + case SubNode sub -> sub.mode()==2 ? new SubFRISC(sub) : sub(sub); + case ToFloatNode tfn -> i2f8(tfn); + case ToIntegerNode cvt -> new F8ToIRISC(cvt); + case XorNode xor -> xor(xor); + + case LoopNode loop -> new LoopNode(loop); + case RegionNode region -> new RegionNode(region); + default -> throw Utils.TODO(); + }; + } + + private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFRISC(add); + if( add.in(2) instanceof ConstantNode off2 && off2._con instanceof TypeInteger ti && imm12(ti) ) + return new AddIRISC(add, (int)ti.value(),true); + return new AddRISC(add); + } + + private Node and(AndNode and) { + if( and.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti ) { + if( imm12(ti) ) + return new AndIRISC(and, (int)ti.value()); + // Could be any size low bit mask + if( ti.value() == 0xFFFFFFFFL ) + return new SrlIRISC(new SllIRISC(and,32),32,false); + } + return new AndRISC(and); + } + + private Node call(CallNode call) { + return call.fptr() instanceof FunPtrNode fptr + ? new CallRISC(call, (TypeFunPtr)fptr._type) + : call.fptr() instanceof ConstantNode con + ? new CallRISC(call, (TypeFunPtr)con._type) + : new CallRRISC(call); + } + private Node nnn(NewNode nnn) { + // TODO: pass in the TFP for alloc + return new NewRISC(nnn); + } + + private Node cmp(BoolNode bool) { + // Float variant directly implemented in hardware + if( bool.isFloat() ) + return new SetFRISC(bool); + + // Only < and y - swap; y < x + // x >= y - swap and negate; !(x < y); `slt tmp=y,x;` then NOT. + // x != y - sub and vs0 == `sub tmp=x-y; sltu dst=tmp,#1` then NOT. + + // The ">", ">=" and "!=" in Simple include a NotNode, which can be + // implemented with a XOR. If one of the above is followed by a NOT + // we can remove the double XOR in the encodings. + + return switch( bool.op() ) { + case "<" -> bool.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti) + ? new SetIRISC(bool, (int)ti.value(),false) + : new SetRISC(bool, false); + // x <= y - flip and negate; !(y < x); `slt tmp=y,x; xori dst=tmp,#1` + case "<=" -> new XorIRISC(new SetRISC(bool.swap12(), false),1,false); + // x == y - sub and vs0 == `sub tmp=x-y; sltu dst=tmp,#1` + case "==" -> new SetIRISC(new SubRISC(bool),1,true,false); + default -> throw Utils.TODO(); + }; + } + + private Node con( ConstantNode con ) { + if( !con._con.isConstant() ) + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; + return switch( con._con ) { + case TypeInteger ti -> { + // External; without knowing the size assume we have to load something + if( ext!=null ) yield new Int8RISC(con,ext); + if( imm12(ti) ) yield new IntRISC(con); + long x = ti.value(); + if( imm20Exact(ti) ) yield new LUI((int)x); + if( (x<<32)>>32 == x ) { // Signed lower 32-bit immediate + // Here, the low 12 bits get sign-extended, which means if + // bit11 is set, the value is negative and lowers the LUI + // value. Add a bit 12 to compensate + if( ((x>>11)&1)==1 ) x += 0x1000; + yield new AddIRISC(new LUI((int)(x & ~0xFFF)), (int)(x & 0xFFF),false); + } + // Need more complex sequence for larger constants... or a load + // from a constant pool, which does not need an extra register + yield new Int8RISC(con,null); + } + // Load from constant pool + case TypeFloat tf -> new FltRISC(con,ext); + case TypeMemPtr tmp -> new TMPRISC(con,ext); + case TypeFunPtr tfp -> new TFPRISC(tfp,ext); + case TypeNil tn -> throw Utils.TODO(); + // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. + case Type t -> t==Type.NIL ? new IntRISC(con) : ConstantNode.raw(con); + }; + } + + private Node fptr( FunPtrNode con ) { + return new TFPRISC(con); + } + + private Node jmp( IfNode iff ) { + if( iff.in(1) instanceof BoolNode bool && !bool.isFloat() ) { + // if less than or equal switch inputs + String bop = bool.op(); + if( bop.equals("<=") || bop.equals(">") ) + return new BranchRISC(iff, IfNode.swap(bop), bool.in(2), bool.in(1)); + return new BranchRISC(iff, bop, bool.in(1), bool.in(2)); + } + // Vs zero + return new BranchRISC(iff, iff.in(1)==null ? "==" : "!=", iff.in(1), null); + } + + private Node or(OrNode or) { + if( or.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new OrIRISC(or, (int)ti.value()); + return new OrRISC(or); + } + + private Node xor(XorNode xor) { + if( xor.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new XorIRISC(xor, (int)ti.value()); + return new XorRISC(xor); + } + + private Node sra(SarNode sar) { + if( sar.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new SraIRISC(sar, (int)ti.value()); + return new SraRISC(sar); + } + + private Node srl(ShrNode shr) { + if( shr.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new SrlIRISC(shr, (int)ti.value(),true); + return new SrlRISC(shr); + } + + private Node sll(ShlNode sll) { + if( sll.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new SllIRISC(sll, (int)ti.value()); + return new SllRISC(sll); + } + + private Node sub(SubNode sub) { + return sub.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti) + ? new AddIRISC(sub, (int)(-ti.value()),true) + : new SubRISC(sub); + } + + private Node i2f8(ToFloatNode tfn) { + assert tfn.in(1)._type instanceof TypeInteger ti; + return new I2F8RISC(tfn); + } + + private Node prj(ProjNode prj) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); + TypeInteger ret = cReturn(prj); + if( ret!=null ) return new ExtendRISC(prj,ret); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjRISC(prj); + } + + private Node ld(LoadNode ld) { + return new LoadRISC(ld,address(ld),off); + } + + private Node st(StoreNode st) { + Node xval = st.val() instanceof ConstantNode con && con._con == TypeInteger.ZERO ? null : st.val(); + return new StoreRISC(st,address(st), off, xval); + } + + // Gather addressing mode bits prior to constructing. This is a builder + // pattern, but saving the bits in a *local* *global* here to keep mess + // contained. + private static int off; + private Node address( MemOpNode mop ) { + off = 0; // Reset + Node base = mop.ptr(); + // Skip/throw-away a ReadOnly, only used to typecheck + if( base instanceof ReadOnlyNode read ) base = read.in(1); + assert !(base instanceof AddNode) && base._type instanceof TypeMemPtr; // Base ptr always, not some derived + if( mop.off() instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti) ) { + off = (int)ti.value(); + } else { + base = new AddRISC(base,mop.off()); + base._type = mop.ptr()._type; + } + return base; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java new file mode 100644 index 00000000..74af9266 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class AddFMemX86 extends MemOpX86 { + AddFMemX86( AddNode add, LoadNode ld , Node base, Node idx, int off, int scale, Node val ) { + super(add,ld, base, idx, off, scale, 0, val ); + } + @Override public String op() { return "addf"+_sz; } + @Override public RegMask regmap(int i) { + if( i==1 ) return null; // Memory + if( i==2 ) return x86_64_v2.RMASK; // base + if( i==3 ) return x86_64_v2.RMASK; // index + if( i==4 ) return x86_64_v2.XMASK; // value + throw Utils.TODO(); + } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 4; } + @Override public void encoding( Encoding enc ) { + X86.memory(enc,0xF2,0x0F58,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(ptr()),enc.reg(idx()),_off,_scale); + } + + // General form: "add dst = src + [base + idx<<2 + 12]" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = "); + sb.p(val()==null ? "#"+_imm : code.reg(val())).p(" + "); + asm_address(code,sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java new file mode 100644 index 00000000..c7d0cf9c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddFX86 extends MachConcreteNode implements MachNode { + AddFX86( Node addf ) { super(addf); } + @Override public String op() { return "addf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 1; } + @Override public boolean commutes() { return true; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0xF2,0x0F58,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + + // General form: "addf dst += src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" += ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java new file mode 100644 index 00000000..0e652a93 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class AddIX86 extends ImmX86 { + AddIX86( Node add, int imm ) { super(add,imm); } + @Override public String op() { + return _imm == 1 ? "inc" : (_imm == -1 ? "dec" : "addi"); + } + @Override public String glabel() { return "+"; } + @Override int opcode() { return 0x81; } + @Override int mod() { return 0; } + @Override public final void encoding( Encoding enc ) { + if( _imm== 1 || _imm== -1 ) { + short dst = enc.reg(this); // Also src1 + X86.unary(enc,0xFF,_imm==1 ? 0 : 1,dst); + } else super.encoding(enc); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java new file mode 100644 index 00000000..ed65b0a8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddMemX86 extends MemOpX86 { + AddMemX86( AddNode add, LoadNode ld , Node base, Node idx, int off, int scale, int imm, Node val ) { + super(add,ld, base, idx, off, scale, imm, val ); + } + @Override public String op() { return "add"+_sz; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 4; } + @Override public void encoding( Encoding enc ) { + X86.memory(enc,0,0x03,true,enc.reg(this),enc.reg(ptr()),enc.reg(idx()),_off,_scale); + } + + // General form: "add dst += [base + idx<<2 + 12]" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" += "); + asm_address(code,sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java new file mode 100644 index 00000000..84543e8f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class AddX86 extends RegX86 { + AddX86( Node add ) { super(add); } + @Override public String op() { return "add"; } + @Override public String glabel() { return "+"; } + @Override int opcode() { return 0x03; } + @Override public boolean commutes() { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java new file mode 100644 index 00000000..3c1b567c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class AndIX86 extends ImmX86 { + AndIX86( Node add, int imm ) { super(add,imm); } + @Override public String op() { return "andi"; } + @Override public String glabel() { return "&"; } + @Override int opcode() { return 0x81; } + @Override int mod() { return 4; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndX86.java new file mode 100644 index 00000000..dedbce45 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class AndX86 extends RegX86 { + AndX86( Node add ) { super(add); } + @Override public String op() { return "and"; } + @Override public String glabel() { return "&"; } + @Override int opcode() { return 0x23; } + @Override public boolean commutes() { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java new file mode 100644 index 00000000..f97163eb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java @@ -0,0 +1,30 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.CallNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; + +public class CallRX86 extends CallNode implements MachNode { + CallRX86( CallNode call ) { super(call); } + @Override public String op() { return "callr"; } + @Override public String label() { return op(); } + @Override public RegMask regmap(int i) { + return i==_inputs._len + ? x86_64_v2.WMASK // Function call target + : x86_64_v2.callInMask(tfp(),i,fun()._maxArgSlot); // Normal argument + } + @Override public RegMask outregmap() { return null; } + @Override public void encoding( Encoding enc ) { + X86.callRegister(enc,enc.reg(fptr())); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(fptr())).p(" "); + for( int i=0; i=x86_64_v2.XMM_OFFSET; + if( xmm && decl.log_size()==3 ) decl=TypeFloat.F64; + else if( !xmm && decl instanceof TypeFloat ) + decl=decl==TypeFloat.F32 ? TypeInteger.U32 : TypeInteger.BOT; + boolean signed=decl.isa(TypeInteger.I8) || decl==TypeInteger.I16 || decl==TypeInteger.I32; + X86.load(enc,decl.log_size(),signed,xmm, + xmm ? dst-x86_64_v2.XMM_OFFSET : dst,ptr,idx,off,scale); + } + + + // General form: "ldN dst,[base + idx<<2 + 12]" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(","); + asm_address(code,sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java new file mode 100644 index 00000000..2e18a058 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java @@ -0,0 +1,28 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MemAddX86 extends MemOpX86 { + MemAddX86( StoreNode st, Node base, Node idx, int off, int scale, int imm, Node val ) { + super(st, st, base, idx, off, scale, imm, val ); + } + @Override public String op() { + return (_imm == 1 ? "inc" : (_imm == -1 ? "dec" : "add")) + _sz; + } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return null; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + @Override public void encoding( Encoding enc ) { + X86.addMem(enc,enc.reg(val()),_imm,enc.reg(ptr()),enc.reg(idx()),_off,_scale); + } + // General form: "add [base + idx<<2 + 12] += src" + @Override public void asm(CodeGen code, SB sb) { + asm_address(code,sb); + if( val()==null ) { + if( _imm != 1 && _imm != -1 ) sb.p(" += #").p(_imm); + } else sb.p(" += ").p(code.reg(val())); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java new file mode 100644 index 00000000..fcca7f8f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java @@ -0,0 +1,89 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import java.lang.StringBuilder; + +// Generic X86 memory operand base. +// inputs: +// ctrl - will appear for possibly RCE (i.e. array ops) +// mem - memory dependence edge +// base - Basic object pointer +// idx/null - Scaled index offset, or null if none +// val/null - Value to store, as part of an op-to-mem or op-from-mem. Null for loads, or if an immediate is being used +// Constants: +// offset - offset added to base. Can be zero. +// scale - scale on index; only 0,1,2,4,8 allowed, 0 is only when index is null +// imm - immediate value to store or op-to-mem, and only when val is null +public abstract class MemOpX86 extends MemOpNode implements MachNode { + final int _off; // Limit 32 bits + final int _scale; // Limit 0,1,2,3 + final int _imm; // Limit 32 bits + final char _sz; // Handy print name + MemOpX86( Node op, MemOpNode mop, Node base, Node idx, int off, int scale, int imm ) { + super(op,mop); + assert base._type instanceof TypeMemPtr && !(base instanceof AddNode); + assert (idx==null && scale==0) || (idx!=null && 0<= scale && scale<=3); + + // Copy memory parts from e.g. the LoadNode over the opcode, e.g. an Add + if( op != mop ) { + _inputs.set(0,mop.in(0)); // Control from mem op + _inputs.set(1,mop.in(1)); // Memory from mem op + _inputs.set(2,base); // Base handed in + } + + assert ptr() == base; + _inputs.setX(3,idx); + _off = off; + _scale = scale; + _imm = imm; + int size = mop instanceof StoreNode st ? st.storeSize() : 1<<_con.log_size(); + _sz = (char)('0'+size); + } + + // Store-based flavors have a value edge + MemOpX86( Node op, MemOpNode mop, Node base, Node idx, int off, int scale, int imm, Node val ) { + this(op,mop,base,idx,off,scale,imm); + _inputs.setX(4,val); + } + + Node idx() { return in(3); } + Node val() { return in(4); } // Only for stores, including op-to-memory + + @Override protected ExprPrinter _printMach(ExprPrinter p) { + return p.p(".").p(_name); + } + + @Override public String label() { return op(); } + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { + if( i==1 ) return null; // Memory + if( i==2 ) return x86_64_v2.RMASK; // base in GPR + if( i==3 ) return x86_64_v2.RMASK; // index in GPR + if( i==4 ) return _sz >= '4' + ? x86_64_v2.MEM_MASK // value in GPR or XMM + : x86_64_v2.RMASK; // Bytes and shorts in GPR only + return null; // Anti-dependence + } + + // "[base + idx<<2 + 12]" + SB asm_address(CodeGen code, SB sb) { + sb.p("[").p(code.reg(ptr())); + if( idx() != null ) { + sb.p("+").p(code.reg(idx())); + if( _scale != 0 ) + sb.p("*").p((1<<_scale)); + } + if( _off != 0 ) + sb.p("+").p(_off); + return sb.p("]"); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java new file mode 100644 index 00000000..74616db7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MulFX86 extends MachConcreteNode implements MachNode { + MulFX86( Node mulf) { super(mulf); } + @Override public String op() { return "mulf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 1; } + @Override public boolean commutes() { return true; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0xF2,0x0F59,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + + // General form: "mulf dst *= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" *= ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java new file mode 100644 index 00000000..5dcb253d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MulIX86 extends MachConcreteNode implements MachNode { + final int _imm; + MulIX86( Node add, int imm ) { + super(add); + _inputs.pop(); // Pop the constant input + _imm = imm; // Record the constant + } + @Override public String op() { return "muli"; } + @Override public String glabel() { return "*"; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + // Rare 3-address form + @Override public final void encoding( Encoding enc ) { + X86.imul(enc,enc.reg(this),enc.reg(in(1)),_imm); + } + // General form: "addi dst += #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java new file mode 100644 index 00000000..6f3fcdeb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MulX86 extends MachConcreteNode implements MachNode { + MulX86( Node mul ) { super(mul); } + @Override public String op() { return "mul"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + @Override public boolean commutes() { return true; } + + @Override public void encoding( Encoding enc ) { + X86.reg(enc,0x0FAF,enc.reg(this),enc.reg(in(2))); + } + + // General form: "mul dst *= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java new file mode 100644 index 00000000..86610f6d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class NegX86 extends MachConcreteNode implements MachNode { + NegX86(MinusNode not) { super(not); } + @Override public String op() { return "neg"; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.RMASK; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding( Encoding enc ) { + X86.unary(enc,0xF7,3,enc.reg(this)); + } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java new file mode 100644 index 00000000..d6a06c2f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; + +public class NewX86 extends NewNode implements MachNode { + // A pre-zeroed chunk of memory. + NewX86( NewNode nnn ) { super(nnn); } + @Override public void encoding( Encoding enc ) { + enc.external(this,"calloc"); + X86.constant(enc,_arg2Reg,1); + X86.call(enc); + } + // General form: "alloc #bytes PC" + @Override public void asm(CodeGen code, SB sb) { + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); + sb.p("call #calloc"); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java new file mode 100644 index 00000000..1e4e7c26 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class NotX86 extends MachConcreteNode implements MachNode { + NotX86(NotNode not) { super(not); } + @Override public String op() { return "not"; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.RMASK; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + + @Override public void encoding( Encoding enc ) { + assert !(in(1) instanceof NotNode); // Cleared out by peeps + assert !(in(1) instanceof BoolNode); + X86.not(enc,enc.reg(this),enc.reg(in(1))); + } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java new file mode 100644 index 00000000..15f4824d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class OrIX86 extends ImmX86 { + OrIX86( Node add, int imm ) { super(add,imm); } + @Override public String op() { return "ori"; } + @Override public String glabel() { return "|"; } + @Override int opcode() { return 0x81; } + @Override int mod() { return 1; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrX86.java new file mode 100644 index 00000000..bc8f0ea8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class OrX86 extends RegX86 { + OrX86( Node add ) { super(add); } + @Override public String op() { return "or"; } + @Override public String glabel() { return "|"; } + @Override int opcode() { return 0x0B; } + @Override public boolean commutes() { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java new file mode 100644 index 00000000..5cfdd804 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ParmNode; + +public class ParmX86 extends ParmNode implements MachNode { + final RegMask _rmask; + ParmX86( ParmNode parm ) { + super(parm); + _rmask = x86_64_v2.callInMask(fun().sig(),_idx,1/*RPC*/); + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return _rmask; } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java new file mode 100644 index 00000000..046c7b4c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ProjNode; + +public class ProjX86 extends ProjNode implements MachNode { + ProjX86( ProjNode p ) { super(p); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { + return ((MachNode)in(0)).outregmap(_idx); + } + @Override public void encoding( Encoding enc ) {} +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RegX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RegX86.java new file mode 100644 index 00000000..06755143 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RegX86.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public abstract class RegX86 extends MachConcreteNode { + RegX86( Node add ) { super(add); } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + @Override public int twoAddress() { return 1; } + abstract int opcode(); + @Override public void encoding( Encoding enc ) { + X86.reg(enc,opcode(),enc.reg(in(1)),enc.reg(in(2))); + } + // General form: "add dst += src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java new file mode 100644 index 00000000..e4467bb4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; + +public class RetX86 extends ReturnNode implements MachNode { + RetX86( ReturnNode ret, FunNode fun ) { super(ret, fun); fun.setRet(this); } + @Override public void encoding( Encoding enc ) { + int sz = fun()._frameAdjust; + if( sz!=0 ) X86.imm(enc,0x81,0,x86_64_v2.RSP,sz); + X86.ret(enc); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarIX86.java new file mode 100644 index 00000000..cf6275dc --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarIX86.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +// Arithmetic Right Shift +public class SarIX86 extends MachConcreteNode implements MachNode { + final int _imm; + SarIX86( Node sari, int imm ) { super(sari); assert x86_64_v2.imm8(imm); _inputs.pop(); _imm = imm; } + @Override public String op() { return "sari"; } + @Override public String glabel() { return ">>"; } + int opcode() { return 0xC1; } + int mod() { return 7; } + + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding(Encoding enc) { + X86.shift(enc,mod(),enc.reg(this),_imm); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java new file mode 100644 index 00000000..92493712 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class SarX86 extends RegX86 { + SarX86( Node add ) { super(add); } + @Override public String op() { return "sar"; } + @Override public String glabel() { return ">>"; } + @Override public RegMask regmap(int i) { + if (i == 1) return x86_64_v2.WMASK; + if (i == 2) return x86_64_v2.RCX_MASK; + throw Utils.TODO(); + } + @Override int opcode() { return 0xD3; } + @Override public void encoding( Encoding enc ) { + X86.unary(enc,opcode(),7,enc.reg(this)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java new file mode 100644 index 00000000..864c1b55 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Corresponds to the x86 instruction "sete && setne". +// Use result of comparison without jump. +public class SetX86 extends MachConcreteNode implements MachNode { + final String _bop; // One of <,<=,== + final boolean _unsigned; + // Constructor expects input is an X86 and not an Ideal node. + SetX86( Node cmp, String bop, boolean unsigned ) { + super(cmp); + _inputs.setLen(1); // Pop the cmp inputs + // Replace with the matched cmp + _inputs.push(cmp); + _bop = bop; + _unsigned = unsigned; + } + @Override public String op() { return "set"+_bop; } + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.FLAGS_MASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + @Override public void encoding( Encoding enc ) { + int op=switch (_bop) { + case "==" -> 0x94; // SETE + case "<" -> _unsigned ? 0x92 : 0x9C; // SETB /SETL + case "<=" -> _unsigned ? 0x96 : 0x9E; // SETBE/SETLE + case ">=" -> _unsigned ? 0x93 : 0x9D; // SETAE/SETGE + case ">" -> _unsigned ? 0x97 : 0x9F; // SETA /SETG + default -> throw Utils.TODO(); + }; + X86.set(enc,op&15,enc.reg(this)); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)); + String src = code.reg(in(1)); + if( src!="flags" ) sb.p(" = ").p(src); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java new file mode 100644 index 00000000..1e62243e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class ShlIX86 extends MachConcreteNode implements MachNode { + final int _imm; + ShlIX86( Node shli, int imm ) { + super(shli); + assert x86_64_v2.imm8(imm); + _inputs.pop(); // Pop constant input off + _imm = imm; + } + public int opcode() { return 0xC1; } + public int mod() { return 4; } + + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + + @Override public String op() { return "shli"; } + @Override public String glabel() { return "<<"; } + @Override public void encoding(Encoding enc) { + X86.shift(enc,mod(),enc.reg(this),_imm); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlX86.java new file mode 100644 index 00000000..768d97e7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class ShlX86 extends RegX86 { + ShlX86( Node add ) { super(add); } + @Override public String op() { return "shl"; } + @Override public String glabel() { return "<<"; } + @Override public RegMask regmap(int i) { + if (i == 1) return x86_64_v2.WMASK; + if (i == 2) return x86_64_v2.RCX_MASK; + throw Utils.TODO(); + } + @Override int opcode() { return 0xD3; } + @Override public final void encoding( Encoding enc ) { + X86.unary(enc,opcode(),4,enc.reg(this)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java new file mode 100644 index 00000000..8a0d1b58 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class ShrIX86 extends MachConcreteNode implements MachNode { + final int _imm; + ShrIX86( Node shri, int imm ) { super(shri); assert x86_64_v2.imm8(imm); _inputs.pop(); _imm = imm; } + @Override public String op() { return "shri"; } + @Override public String glabel() { return ">>>"; } + int opcode() { return 0xC1; } + int mod() { return 5; } + + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding(Encoding enc) { + X86.shift(enc,mod(),enc.reg(this),_imm); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrX86.java new file mode 100644 index 00000000..1ba20fee --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class ShrX86 extends RegX86 { + ShrX86( Node add ) { super(add); } + @Override public String op() { return "shr"; } + @Override public String glabel() { return ">>>"; } + @Override public RegMask regmap(int i) { + if (i == 1) return x86_64_v2.WMASK; + if (i == 2) return x86_64_v2.RCX_MASK; + throw Utils.TODO(); + } + @Override int opcode() { return 0xD3; } + @Override public final void encoding( Encoding enc ) { + X86.unary(enc,opcode(),5,enc.reg(this)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java new file mode 100644 index 00000000..cb7694f9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; + +// Flag snapshots can use GPRs or stack slots, but never XMM registers. +public class SplitFlagsX86 extends SplitX86 { + private static final RegMask MASK = new RegMask(x86_64_v2.WR_BITS | (1L<=x86_64_v2.MAX_REG ) + X86.flagsStack(enc,fun(enc).computeStackOffset(enc._code,reg),toFlags); + else + X86.flags(enc,reg,toFlags); + } else super.encoding(enc); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java new file mode 100644 index 00000000..46f2cb16 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.SplitNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class SplitX86 extends SplitNode { + SplitX86( LRG lrg, String kind, byte round ) { super(lrg,kind,round, new Node[2]); } + @Override public RegMask regmap(int i) { return x86_64_v2.SPLIT_MASK; } + @Override public RegMask outregmap() { return x86_64_v2.SPLIT_MASK; } + + // Moves among GPRs, XMM registers, and stack slots. + @Override public void encoding( Encoding enc ) { + short dst=enc.reg(this), src=enc.reg(in(1)); + assert dst!=x86_64_v2.FLAGS && src!=x86_64_v2.FLAGS; + boolean dstX=dst>=x86_64_v2.XMM_OFFSET, srcX=src>=x86_64_v2.XMM_OFFSET; + // Stack spills + if( dst >= x86_64_v2.MAX_REG ) { + int off = fun(enc).computeStackOffset(enc._code,dst); + if( src >= x86_64_v2.MAX_REG ) { + // Rare stack-stack move. push [RSP+soff]; pop [RSP+doff] + int soff = fun(enc).computeStackOffset(enc._code,src); + X86.stackCopy(enc,soff,off); + return; + } + StoreX86.encVal(enc, srcX ? TypeFloat.F64 : TypeInteger.BOT, (short)x86_64_v2.RSP, (short)-1/*index*/, src, off, 0); + return; + } + if( src >= x86_64_v2.MAX_REG ) { + int off = fun(enc).computeStackOffset(enc._code,src); + LoadX86.enc(enc, dstX ? TypeFloat.F64 : TypeInteger.BOT, dst, (short)x86_64_v2.RSP, (short)-1, off, 0); + return; + } + + X86.move(enc,dstX,srcX,dstX ? dst-x86_64_v2.XMM_OFFSET : dst, + srcX ? src-x86_64_v2.XMM_OFFSET : src); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java new file mode 100644 index 00000000..9ffa25fc --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java @@ -0,0 +1,48 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StoreNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.util.SB; + +public class StoreX86 extends MemOpX86 { + StoreX86( StoreNode st, Node base, Node idx, int off, int scale, int imm, Node val ) { + super(st,st, base, idx, off, scale, imm, val); + } + @Override public String op() { return "st"+_sz; } + @Override protected ExprPrinter _printMach(ExprPrinter p) { + Node val = val(); + p.p(".").p(_name).p("="); + if( val==null ) p.p(_imm); + else p.n(val); + return p.p(";"); + } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return null; } + @Override public void encoding( Encoding enc ) { + short ptr=enc.reg(ptr()), idx=enc.reg(idx()), src=enc.reg(val()); + if( src==-1 ) X86.storeImm(enc,Integer.numberOfTrailingZeros(_sz-'0'),_imm,ptr,idx,_off,_scale); + else encVal(enc,_con,ptr,idx,src,_off,_scale); + } + + // Non-immediate encoding + static void encVal( Encoding enc, Type decl, short ptr, short idx, short src, int off, int scale ) { + boolean xmm=src>=x86_64_v2.XMM_OFFSET; + X86.store(enc,decl.log_size(),xmm, + xmm ? src-x86_64_v2.XMM_OFFSET : src,ptr,idx,off,scale); + } + + + // General form: "stN [base + idx<<2 + 12],val" + @Override public void asm(CodeGen code, SB sb) { + asm_address(code,sb).p(","); + if( val()==null ) sb.p("#").p(_imm); + else sb.p(code.reg(val())); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java new file mode 100644 index 00000000..29f0033a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class SubFX86 extends MachConcreteNode implements MachNode { + SubFX86( Node subf) { super(subf); } + @Override public String op() { return "subf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0xF2,0x0F5C,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + + // General form: "subf dst -= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" -= ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java new file mode 100644 index 00000000..8da34bf5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java @@ -0,0 +1,10 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class SubX86 extends RegX86 { + SubX86( Node add ) { super(add); } + @Override public String op() { return "sub"; } + @Override public String glabel() { return "-"; } + @Override int opcode() { return 0x2B; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java new file mode 100644 index 00000000..e3563713 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java @@ -0,0 +1,51 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Function constants +public class TFPX86 extends FunPtrNode implements MachNode, RIPRelSize { + private byte _opLen; + final String _ext; // External name + // Pointer to a Simple function + TFPX86( FunPtrNode fptr ) { this((TypeFunPtr)fptr._type,null); } + // Spill clone: copy inputs without registering their reverse edges; + // insertBefore will place and register the clone in its use block. + private TFPX86( TFPX86 fptr ) { super(fptr); _ext = fptr._ext; } + // Pointer to an Extern "C" function + TFPX86( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } + @Override public String op() { return "ldx"; } + @Override public String label() { return op(); } + @Override public boolean isClone() { return true; } + @Override public TFPX86 copy() { return new TFPX86(this); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public void encoding( Encoding enc ) { + if( _ext==null ) enc.relo(this); + else enc.external(this,_ext); + _opLen=X86.rip(enc,0,0x8D,true,enc.reg(this)); + } + + @Override public byte encSize(int delta) { return _opLen; } + + // Delta is from opcode start, X86 expects it from opcode end + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + enc.patch4(opStart + opLen - 4, delta - opLen); + } + + // Human-readable form appended to the SB. Things like the encoding, + // indentation, leading address or block labels not printed here. + // Just something like "ld4\tR17=[R18+12] // Load array base". + // General form: "op\tdst=src+src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)); + if( _ext == null ) _type.print(sb.p(" #")); + else sb.p(_ext); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java new file mode 100644 index 00000000..682597f4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.util.SB; + +public class TMPX86 extends ConstantNode implements MachNode, RIPRelSize{ + private byte _opLen; + final String _ext; + TMPX86( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ldp"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public boolean isClone() { return true; } + @Override public Node copy() { return new TMPX86(this,_ext); } + + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this); + // 0 or 1 for REX depending on the dst. + _opLen = X86.rip(enc,0,0x8D,true,dst); + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj, _opLen-4, 2/*ELF encoding PC32*/); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { return _opLen; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + enc.patch4(opStart + opLen - 4, delta - opLen); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" = #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/UJmpX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/UJmpX86.java new file mode 100644 index 00000000..591a51c3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/UJmpX86.java @@ -0,0 +1,41 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// unconditional jump +public class UJmpX86 extends CFGNode implements MachNode, RIPRelSize { + UJmpX86() { } + @Override public String op() { return "jmp"; } + @Override public String label() { return op(); } + @Override protected String format() { return "jmp "; } + @Override public RegMask regmap(int i) {return null; } + @Override public RegMask outregmap() { return null; } + @Override public Type compute() { throw Utils.TODO(); } + @Override public Node idealize() { throw Utils.TODO(); } + @Override public void encoding( Encoding enc ) { + enc.jump(this,uctrl()); + X86.branch(enc,-1); + } + + // Delta is from opcode start, but X86 measures from the end of the 2-byte encoding + @Override public byte encSize(int delta) { + return X86.branchSize(delta,false); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + X86.patchBranch(enc.bits(),opStart,opLen,delta,-1); + } + + @Override public void asm(CodeGen code, SB sb) { + CFGNode target = uctrl(); + //assert target.nOuts() > 1; // Should optimize jmp to empty targets + sb.p(label(target)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java new file mode 100644 index 00000000..2d989a50 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class XorIX86 extends ImmX86 { + XorIX86( Node add, int imm ) { super(add,imm); } + @Override public String op() { return "xori"; } + @Override public String glabel() { return "^"; } + @Override int opcode() { return 0x81; } + @Override int mod() { return 6; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorX86.java new file mode 100644 index 00000000..86703903 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class XorX86 extends RegX86 { + XorX86( Node add ) { super(add); } + @Override public String op() { return "xor"; } + @Override public String glabel() { return "^"; } + @Override int opcode() { return 0x33; } + @Override public boolean commutes() { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java new file mode 100644 index 00000000..3dc93bf6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java @@ -0,0 +1,596 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class x86_64_v2 extends Machine { + public x86_64_v2( CodeGen code ) {} + // X86-64 V2. Includes e.g. SSE4.2 and POPCNT. + @Override public String name() { return "x86_64_v2"; } + @Override public int defaultOpSize() { return 5; } + + public static final int RAX = 0, RCX = 1, RDX = 2, RBX = 3, RSP = 4, RBP = 5, RSI = 6, RDI = 7; + public static final int R08 = 8, R09 = 9, R10 = 10, R11 = 11, R12 = 12, R13 = 13, R14 = 14, R15 = 15; + + public static final int XMM0 = 16, XMM1 = 17, XMM2 = 18, XMM3 = 19, XMM4 = 20, XMM5 = 21, XMM6 = 22, XMM7 = 23; + public static final int XMM8 = 24, XMM9 = 25, XMM10 = 26, XMM11 = 27, XMM12 = 28, XMM13 = 29, XMM14 = 30, XMM15 = 31; + public static final int FLAGS = 32; + public static final int MAX_REG = 33; + public static final int RPC = 33; + + // Human-readable name for a register number, e.g. "RAX". + public static final String[] REGS = new String[]{ + "rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", + "r8" , "r9" , "r10", "r11", "r12", "r13", "r14", "r15", + "xmm0", "xmm1", "xmm2" , "xmm3" , "xmm4" , "xmm5" , "xmm6" , "xmm7" , + "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15", + "flags", + }; + // Hard crash for bad register number, fix yer bugs! + @Override public String[] regs() { return REGS; } + + public static int XMM_OFFSET = 16; + // General purpose register mask: pointers and ints, not floats + static final long RD_BITS = 0b1111111111111111L; // All the GPRs + static RegMask RMASK = new RegMask(RD_BITS); + // No RSP in the *write* general set. + static final long WR_BITS = 0b1111111111101111L; // All the GPRs minus RSP + static RegMask WMASK = new RegMask(WR_BITS); + // Xmm register mask + static final long FP_BITS = 0b1111111111111111L << XMM0; // All the XMMs + static RegMask XMASK = new RegMask(FP_BITS); + static RegMask FLAGS_MASK = new RegMask(FLAGS); + static RegMask RPC_MASK = new RegMask(RPC); + // Divide mask: exclude RDX:RAX + static RegMask DIV_MASK = new RegMask(RD_BITS & ~(1L< 0x84; + case "!=" -> 0x85; + case ">" -> 0x8F; + case "<" -> 0x8C; + case "<=" -> 0x8E; + case ">=" -> 0x8D; + default -> throw new IllegalArgumentException("Too many arguments"); + }; + } + + // Limit of float args passed in registers + static RegMask[] XMMS8 = new RegMask[]{ + new RegMask(XMM0), new RegMask(XMM1), new RegMask(XMM2), new RegMask(XMM3), + new RegMask(XMM4), new RegMask(XMM5), new RegMask(XMM6), new RegMask(XMM7), + }; + + // Map from function signature and argument index to register. + // Used to set input registers to CallNodes, and ParmNode outputs. + @Override public RegMask callArgMask( TypeFunPtr tfp, int idx, int maxArgSlot ) { return callInMask(tfp,idx,maxArgSlot); } + @Override public RegMask callArgMask( TypeFunPtr tfp, int idx, int maxArgSlot, String conv ) { return callInMask(tfp,idx,maxArgSlot,conv); } + static RegMask callInMask( TypeFunPtr tfp, int idx, int maxArgSlot ) { + return callInMask(tfp,idx,maxArgSlot,CodeGen.CODE._callingConv); + } + static RegMask callInMask( TypeFunPtr tfp, int idx, int maxArgSlot, String conv ) { + if( idx==0 ) return RPC_MASK; + if( idx==1 ) return null; + if( idx-2 >= tfp.nargs() ) return null; // Anti-dependence + return switch( conv ) { + case "SystemV" -> callSys5 (tfp,idx,maxArgSlot); + case "win64" -> callWin64(tfp,idx,maxArgSlot); + default -> throw Utils.TODO(); + }; + } + + // Maximum stack args used by this signature + @Override public short maxArgSlot( TypeFunPtr tfp ) { + return maxArgSlot(tfp,CodeGen.CODE._callingConv); + } + @Override public short maxArgSlot( TypeFunPtr tfp, String conv ) { + return switch( conv ) { + case "SystemV" -> maxArgSlotSys5 (tfp); + case "win64" -> maxArgSlotWin64(tfp); + default -> throw Utils.TODO(); + }; + } + + // Win64 - max *4* args in registers of all kinds; after that on stack. + // Stack args land in increasing memory order with empty/mirror slots for + // every register. + // + // foo( int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, ... ) + // + // -- Prior Frame -- + // i6 + // f5 + // i4 + // empty mirror, XMM3= f3 + // empty mirror, R08 = i2 + // empty mirror, XMM1= f1 + // empty mirror, RCX = i0 + // -- Caller Frame; 16b align -- + // RPC + // PAD/ALIGN + // -- Callee Frame; 16b align -- + static RegMask[] WIN64_CALL = new RegMask[] { + RCX_MASK, + RDX_MASK, + R08_MASK, + R09_MASK, + }; + + static RegMask callWin64(TypeFunPtr tfp, int idx, int maxArgSlot ) { + // idx 2,3,4,5 passed in registers, with stack slot mirrors. + // idx >= 6 passed on stack, starting at slot#1 (#0 reserved for RPC). + int sigidx = idx-2; + if( hiddenSelf(tfp) ) { + if( sigidx==0 ) return null; + sigidx--; + } + if( sigidx >= 4 ) + return new RegMask(MAX_REG+maxArgSlot+sigidx); + return tfp.arg(idx-2) instanceof TypeFloat + ? XMMS8 [sigidx] + : WIN64_CALL[sigidx]; + } + static short maxArgSlotWin64(TypeFunPtr tfp) { + return (short)Math.max(4,tfp.nargs() - (hiddenSelf(tfp) ? 1 : 0)); + } + static short maxIncomingArgSlotWin64(TypeFunPtr tfp) { + return (short)Math.max(0,tfp.nargs() - (hiddenSelf(tfp) ? 1 : 0) - 4); + } + + // Sys5: max 6 GPRs and 8 FPRS filled first. Extra args land in increasing + // memory order as needed - no mirror space. + // + // foo( int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, ... ) + // RDI =i0, RSI =i2, RDX =i4, RCX =i6, R08 =i8, R09 =i10 + // XMM0=f1, XMM1=f3, XMM2=f5, XMM3=f7, XMM4=f9, XMM5=f11, XMM6=f13, XMM7=f15 + // + // -- Prior Frame -- + // f18 + // i18 + // f17 + // i16 + // i14 + // i12 + // -- Caller Frame; 16b align -- + // RPC + // PAD/ALIGN + // -- Callee Frame; 16b align -- + // SystemV(param passing) + static RegMask[] SYS5_CALL = new RegMask[] { + RDI_MASK, + RSI_MASK, + RDX_MASK, + RCX_MASK, + R08_MASK, + R09_MASK, + }; + + static RegMask callSys5(TypeFunPtr tfp, int idx, int maxArgSlot ) { + // First 6 integers passed in registers: rdi,rsi,rdx,rcx,r08,r09 + // First 8 floats passed in registers: xmm0-xmm7 + int sigidx = idx-2; + if( hiddenSelf(tfp) ) { + if( sigidx==0 ) return null; + sigidx--; + } + int icnt=0, fcnt=0; // Count of ints, floats + for( int i=hiddenSelf(tfp) ? 1 : 0; i 0 && tfp.arg(0) == TypePtr.PTR; + } + + static final long SYSTEM5_CALLER_SAVE = + (1L << RAX) | (1L << RCX) | (1L << RDX) | + (1L << RDI) | (1L << RSI) | + (1L << R08) | (1L << R09) | (1L << R10) | (1L << R11) | + (1L << FLAGS) | // Flags are killed + // All FP regs are killed + FP_BITS; + + static final long WIN64_CALLER_SAVE = + (1L<< RAX) | (1L<< RCX) | (1L<< RDX) | + (1L<< R08) | (1L<< R09) | (1L<< R10) | (1L<< R11) | + (1L< SYSTEM5_CALLER_SAVE; + case "win64" -> WIN64_CALLER_SAVE; + default -> throw new IllegalArgumentException("Unknown calling convention: " + conv); + }; + } + @Override public String cCallingConv(String callingConv) { + return callingConv; + } + @Override public long neverSave() { return 1L< add(add); + case AndNode and -> and(and); + case BoolNode bool -> cmp(bool); + case CLInitNode init -> new CLInitNode(init); + case CProjNode c -> new CProjNode(c); + case CallEndNode cend -> new CallEndMach(cend); + case CallNode call -> call(call); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); + case ConstantNode con -> con(con); + case DivNode div -> div.mode()==2 ? new DivFX86(div) : new DivX86(div); + case EscapeNode esc -> new EscapeNode(esc); + case FunNode fun -> new FunX86(fun); + case FunPtrNode fptr -> fptr(fptr); + case IfNode iff -> jmp(iff); + case LoadNode ld -> ld(ld); + case MemMergeNode mem -> new MemMergeNode(mem); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegX86(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFX86(mul) : mul(mul); + case NewNode nnn -> new NewX86(nnn); + case NotNode not -> new NotX86(not); + case OrNode or -> or(or); + case ParmNode parm -> new ParmX86(parm); + case BulkMemPhiNode phi-> new BulkMemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); + case PhiNode phi -> new PhiNode(phi); + case ProjNode prj -> prj(prj); + case ReadOnlyNode read-> new ReadOnlyMach(read); + case ReturnNode ret -> new RetX86(ret, ret.fun()); + case SarNode sar -> sar(sar); + case ShlNode shl -> shl(shl); + case ShrNode shr -> shr(shr); + case StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); + case StartNode start -> new StartNode(start); + case StopNode stop -> new StopNode(stop); + case StoreNode st -> st(st); + case SubNode sub -> sub.mode()==2 ? new SubFX86(sub) : sub(sub); + case ToFloatNode tfn -> i2f8(tfn); + case ToIntegerNode cvt -> new F8ToIX86(cvt); + case XCtrlNode x -> ConstantNode.raw(Type.XCONTROL); + case XorNode xor -> xor(xor); + + case LoopNode loop -> new LoopNode(loop); + case RegionNode region -> new RegionNode(region); + default -> throw Utils.TODO(); + }; + } + + public static boolean imm8( long imm ) { return -128 <= imm && imm <= 127; } + + public static boolean imm32( long imm ) { return (int)imm==imm; } + + // Attempt a full LEA-style break down. + private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFX86(add); + Node lhs = add.in(1); + Node rhs = add.in(2); + if( lhs instanceof LoadNode ld && ld.nOuts() == 1 && ld.declaredType().log_size() >= 3) + return new AddMemX86(add, address(ld), ld.ptr(), idx, off, scale, 0, rhs); + // Attempt a full LEA-style break down. + // Returns one of AddX86, AddIX86, LeaX86, or LHS + if( rhs instanceof ConstantNode off2 && off2._con instanceof TypeInteger toff ) { + long imm = toff.value(); + assert imm!=0; // Folded in peeps + if( (int)imm != imm ) // Full 64bit immediate + return new AddX86(add); + // Now imm <= 32bits + if( lhs instanceof AddNode ladd ) + // ((base + (idx << scale)) + off) + return _lea(add, ladd.in(1), ladd.in(2), (int)imm); + if( lhs instanceof ShlNode shift ) + // (idx << scale) + off; no base + return _lea(add, null, shift, (int)imm); + + // lhs + rhs1 + return new AddIX86(add, (int)imm); + } + return _lea(add, lhs, rhs, 0); + } + + + private Node _lea(Node add, Node base, Node idx, int off) { + int scale = 0; + if( base instanceof ShlNode && !(idx instanceof ShlNode) ) + throw Utils.TODO(); // Bug in canonicalization, should on RHS + if( idx instanceof ShlNode shift && shift.in(2) instanceof ConstantNode shfcon && + shfcon._con instanceof TypeInteger tscale && 0 <= tscale.value() && tscale.value() <= 3 ) { + idx = shift.in(1); + scale = ((int) tscale.value()); + } + // (base + idx) + off + return off == 0 && scale == 0 + ? new AddX86(add) + : new LeaX86(add, base, idx, scale, off); + } + + + private Node and(AndNode and) { + if( and.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new AndIX86(and, (int)ti.value()); + return new AndX86(and); + } + + private Node call(CallNode call) { + return call.fptr() instanceof FunPtrNode con + ? new CallX86(call, (TypeFunPtr)con._type) + : call.fptr() instanceof ConstantNode con && con._type instanceof TypeFunPtr + ? new CallX86(call, (TypeFunPtr)con._type) + : new CallRX86(call); + } + + // Because X86 flags, a normal ideal Bool is 2 X86 ops: a "cmp" and at "setz". + // Ideal If reading from a setz will skip it and use the "cmp" instead. + private static boolean swap, unsigned; + private Node cmp( BoolNode bool ) { + swap = unsigned = false; + Node cmp = _cmp(bool); + return new SetX86(cmp, swap ? IfNode.swap(bool.op()) : bool.op(), unsigned); + } + + private Node _cmp(BoolNode bool) { + // Float variant + if( bool.isFloat() ) { + unsigned = true; + return new CmpFX86(bool); + } + Node lhs = bool.in(1); + Node rhs = bool.in(2); + + // Since cmp does not record a BOP, SetX/Jmp need to know if the CMP is swapped. + + // Vs memory + if( lhs instanceof LoadNode ld && ld.nOuts() == 1 && rhs._type.isa(ld.declaredType()) ) + return new CmpMemX86(bool, address(ld), ld.ptr(), idx, off, scale, imm(rhs), val, false); + + // Operands swap in the encoding directly, no need for Set/Jmp to swap `bop` + if( rhs instanceof LoadNode ld && ld.nOuts() == 1 && lhs._type.isa(ld.declaredType()) && + (!(lhs instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value())) || + bool.op()=="==" || bool.op()=="!=") ) + return new CmpMemX86(bool, address(ld), ld.ptr(), idx, off, scale, imm(lhs), val, true ); + + // Vs immediate + if( rhs instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new CmpIX86(bool, (int)ti.value(), false); + + // Operand swap compare; Set and Jmp need to swap `bop` + if( lhs instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new CmpIX86(bool, (int)ti.value(), swap=true); + + // x vs y + return new CmpX86(bool); + } + + private Node con( ConstantNode con ) { + if( !con._con.isConstant() ) + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; + return switch (con._con) { + case TypeInteger ti -> new IntX86(con,ext); + case TypeFloat tf -> new FltX86(con,ext); + case TypeMemPtr tmp -> new TMPX86(con,ext); + case TypeFunPtr tfp -> new TFPX86(tfp,ext); + case TypeNil tn -> throw Utils.TODO(); + // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. + case Type t -> t == Type.NIL ? new IntX86(con,null) : ConstantNode.raw(con); + }; + } + + private Node fptr( FunPtrNode con ) { + return new TFPX86(con); + } + + private Node i2f8(ToFloatNode tfn) { + assert tfn.in(1)._type instanceof TypeInteger ti; + return new I2f8X86(tfn); + } + + private Node jmp(IfNode iff) { + // If/Bool combos will match to a Cmp/Set which sets flags. + // Most general arith ops will also set flags, which the Jmp needs directly. + // Loads do not set the flags, and will need an explicit TEST + String op = "!="; + if( iff.in(1) instanceof BoolNode bool ) op = swap ? IfNode.swap(bool.op()) : bool.op(); + else if( iff.in(1)==null ) op = "=="; // Never-node cutout + else iff.setDef(1, new BoolNode.NE(iff.in(1), + ConstantNode.raw(TypeInteger.ZERO), + (byte)(iff.in(1)._type instanceof TypeFloat ? 2 : 1))); + return new JmpX86(iff, op); + } + + private Node ld(LoadNode ld) { + return new LoadX86(address(ld), ld.ptr(), idx, off, scale); + } + + private Node mul(MulNode mul) { + if( mul.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new MulIX86(mul, (int)ti.value()); + return new MulX86(mul); + } + + private Node or(OrNode or) { + if( or.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new OrIX86(or, (int)ti.value()); + return new OrX86(or); + } + + private Node prj( ProjNode prj ) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); + TypeInteger ret = cReturn(prj); + if( ret!=null ) return new ExtendX86(prj,ret); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjX86(prj); + } + + private Node sar(SarNode sar) { + if( sar.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm8(ti.value())) + return new SarIX86(sar, (int)(ti.value() & 0xff) ); + return new SarX86(sar); + } + + private Node shl(ShlNode shl) { + if( shl.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm8(ti.value())) + return new ShlIX86(shl, (int)(ti.value() & 0xff) ); + return new ShlX86(shl); + } + + private Node shr( ShrNode shr ) { + if( shr.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm8(ti.value()) ) + return new ShrIX86(shr, (int)(ti.value() & 0xff) ); + return new ShrX86(shr); + } + + private Node st( StoreNode st ) { + // Look for "*ptr op= val" + Node op = st.val(); + if( op instanceof AddNode ) { + if( op.in(1) instanceof LoadNode ld && stld_match(st,ld) ) + return new MemAddX86(address(st), st.ptr(), idx, off, scale, imm(op.in(2)), val); + if( op.in(2) instanceof LoadNode ld && stld_match(st,ld) ) + return new MemAddX86(address(st), st.ptr(), idx, off, scale, imm(op.in(1)), val); + } + return new StoreX86(address(st), st.ptr(), idx, off, scale, imm(st.val()), val); + } + private static boolean stld_match(StoreNode st, LoadNode ld ) { + return + ld.in(0) == st.in(0) && + ld.mem() == st.mem() && + ld.ptr() == st.ptr() && + ld.off() == st.off(); + } + + + private Node sub (SubNode sub ){ + return sub.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti + ? new AddIX86(sub, (int)-ti.value()) + : new SubX86(sub); + } + + private Node xor (XorNode xor){ + if( xor.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new XorIX86(xor, (int)ti.value()); + return new XorX86(xor); + } + + // Gather X86 addressing mode bits prior to constructing. This is a + // builder pattern, but saving the bits in a *local* *global* here to keep + // mess contained. + private static int off, scale, imm; + private static Node idx, val; + private N address(N mop) { + off = scale = imm = 0; // Reset + idx = val = null; + Node base = mop.ptr(); + // Skip/throw-away a ReadOnly, only used to typecheck + if(base instanceof ReadOnlyNode read) base = read.in(1); + assert !(base instanceof AddNode) && base._type instanceof TypeMemPtr; // Base ptr always, not some derived + if(mop.off() instanceof AddNode add && add.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti) { + off = (int) ti.value(); + assert off == ti.value(); // In 32-bit range + idx = add.in(1); + if(idx instanceof ShlNode shift && shift.in(2) instanceof ConstantNode shfcon && + shfcon._con instanceof TypeInteger tscale && 0 <= tscale.value() && tscale.value() <= 3) { + idx = shift.in(1); + scale = (int) tscale.value(); + } + } else { + if(mop.off() instanceof ConstantNode con && con._con instanceof TypeInteger ti) { + off = (int) ti.value(); + assert off == ti.value(); // In 32-bit range + } else { + idx = mop.off(); + } + } + return mop; + } + + private int imm( Node xval ) { + assert val == null && imm == 0; + if( xval instanceof ConstantNode con && con._con instanceof TypeInteger ti && ti.isConstant() && imm32(ti.value()) ) { + val = null; + imm = (int) ti.value(); + assert imm == ti.value(); // In 32-bit range + } else { + val = xval; + } + return imm; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java new file mode 100644 index 00000000..58506e9c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java @@ -0,0 +1,101 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.print.AssemblyAdapter; +import com.seaofnodes.print.ASMPrinter.Data; +import com.seaofnodes.print.ASMPrinter.Pool; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.BAOS; +import java.util.ArrayList; +import com.seaofnodes.simple.codegen.Encoding; +import java.util.HashMap; + +/** Machine-specific facts; traversal, columns and byte display live in print/. */ +public final class ASMPrinter extends AssemblyAdapter { + private final CodeGen _code; + private final Encoding _enc; + private ASMPrinter(CodeGen code) { + super(new IRPrinter()); + _code=code; + _enc=code._encoding; + } + public static SB print(SB sb, CodeGen code) { + return sb.p(new com.seaofnodes.print.ASMPrinter<>(new ASMPrinter(code)).print()); + } + @Override public ArrayList blocks() { + var blocks=new ArrayList(); + var cfg=_enc==null ? _code._cfg : _enc._cfg; + if( cfg!=null ) for( Node n : cfg ) blocks.add(n); + return blocks; + } + @Override public String op(Node n) { return n instanceof MachNode m ? m.op() : n.label(); } + @Override public String register(Node n) { return _code.reg(n); } + @Override public String comment(Node n) { return n.comment(); } + @Override public String phiName(Node n) { return ((PhiNode)n)._label; } + @Override public String operands(Node n) { + SB sb=new SB(); + if( n instanceof MachNode m ) m.asm(_code,sb); + else if( !(n._type instanceof TypeMem) ) { + sb.p(n._nid).p(": "); + for( Node def : n._inputs ) sb.p(def==null ? "___" : Integer.toString(def._nid)).p(' '); + } + return sb.toString(); + } + @Override public boolean labelBlock(Node n) { + return !(n instanceof FunNode) && !(n instanceof IfNode) && !(n instanceof CallNode) && + !(n instanceof CallEndNode) && !(n instanceof CProjNode && ir.input0(n) instanceof CallEndNode); + } + @Override public boolean hidden(Node n) { + if( n instanceof PhiNode || n instanceof ProjNode ) + if( n._type instanceof TypeMem || n._type instanceof TypeRPC ) return true; + if( postAlloc() && n instanceof CalleeSaveNode ) return true; + if( n instanceof MemMergeNode ) return true; + if( n instanceof EscapeNode ) return true; + return n.getClass()==ConstantNode.class; + } + @Override public boolean postAlloc() { return _code._phase.ordinal()>CodeGen.Phase.RegAlloc.ordinal() || + (_code._phase==CodeGen.Phase.RegAlloc && _code._regAlloc!=null && _code._regAlloc.done()); } + @Override public boolean encoded() { return _enc!=null && _code._phase.ordinal()>=CodeGen.Phase.Encoding.ordinal(); } + @Override public int defaultSize() { return _code._mach==null ? 2 : _code._mach.defaultOpSize(); } + @Override public boolean littleEndian() { return _code._asmLittle; } + @Override public boolean prologue(Node n) { return ((FunNode)n)._frameAdjust!=0; } + @Override public byte[] bytes() { return encoded() ? _enc._bits.buf() : null; } + @Override public int offset(Node n) { return encoded() ? _enc.opStart(n) : -1; } + @Override public int size(Node n) { return encoded() ? _enc.opLen(n)&255 : 0; } + private static ArrayList entries(Iterable relos, boolean ro) { + var data=new ArrayList(); + for( Encoding.Relo r : relos ) { + if( r.readOnly()!=ro ) continue; + int align=1< pools(int codeEnd) { + var pools=new ArrayList(); + if( !encoded() ) return pools; + int base=(codeEnd+15)&-16; + if( _enc._cpool.size()>0 ) + pools.add(new Pool("Constant Pool",_enc._cpool.buf(),0,base,objects(true))); + base=(base+_enc._cpool.size()+15)&-16; + if( _enc._sdata.size()>0 ) + pools.add(new Pool("Static Data",_enc._sdata.buf(),0,base,objects(false))); + return pools; + } + private ArrayList objects(boolean ro) { + var data=new ArrayList(); + for( var obj : _enc._data.entries() ) + if( !obj.external && obj.readOnly==ro ) + data.add(new Data(obj,obj.symbol+": "+obj.type.str(),obj.alignment,obj.size)); + return data; + } + // Retain this diagnostic entry point for checking that pool printing never lays out types. + static int printConstantPool(int address, SB sb, BAOS bits, HashMap relos, + boolean ro, String name) { + sb.p(com.seaofnodes.print.ASMPrinter.pool(new Pool(name,bits.buf(),0,address,entries(relos.values(),ro)))); + return address+bits.size(); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/print/IRPrinter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/print/IRPrinter.java new file mode 100644 index 00000000..531a549a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/print/IRPrinter.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.print.IRAdapter; +import com.seaofnodes.simple.node.*; +import java.util.ArrayList; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.codegen.CodeGen; + +/** This chapter's read-only IR facts and the familiar debugger entry points. */ +public final class IRPrinter extends IRAdapter { + private static final com.seaofnodes.print.IRPrinter PRINT = + new com.seaofnodes.print.IRPrinter<>(new IRPrinter()); + @Override public boolean global(Node n) { return n.isConst(); } + @Override public String type(Node n) { return n._type==null ? "" : n._type.str(); } + @Override public String inputMark(Node n, Node def) { return n instanceof MemMergeNode && def instanceof MemMergeNode ? "^" : " "; } + @Override public Kind kind(Node n) { + if( n instanceof StartCUNode ) return Kind.UNIT; + if( n instanceof StopCUNode ) return Kind.UNIT; + if( n instanceof StartNode ) return Kind.START; + if( n instanceof StopNode ) return Kind.STOP; + if( n instanceof FunNode ) return Kind.FUN; + if( n instanceof LoopNode ) return Kind.LOOP; + if( n instanceof RegionNode ) return Kind.REGION; + if( n instanceof ParmNode ) return Kind.PARM; + if( n instanceof PhiNode ) return Kind.PHI; + if( n instanceof CallEndNode ) return Kind.CALL_END; + if( n instanceof CallNode ) return Kind.CALL; + if( n instanceof ReturnNode ) return Kind.RETURN; + if( n instanceof ConstantNode ) return Kind.CONSTANT; + if( n instanceof CProjNode ) return Kind.CPROJ; + if( n instanceof ProjNode ) return n instanceof CFGNode ? Kind.CPROJ : Kind.PROJ; + if( n instanceof CFGNode ) return Kind.CTRL; + if( n instanceof MultiNode ) return Kind.MULTI; + return Kind.DATA; + } + @Override public int index(Node n) { + if( n instanceof ProjNode p ) return p._idx; + if( n instanceof CProjNode p ) return p._idx; + return n._nid; + } + @Override public String functionName(Node n) { return ((FunNode)n)._name==null ? "" : ((FunNode)n)._name; } + @Override public String signature(Node n) { + FunNode fun=(FunNode)n; + return (fun._name==null ? "" : fun._name)+" "+fun.sig().str(); + } + + public static String prettyPrint(Node n, int depth) { return PRINT.prettyPrint(n,depth); } + public static SB printLine(Node n, SB sb) { return sb.p(PRINT.line(n)); } + + public static String prettyPrint(CodeGen code) { + var units=new ArrayList>(); + for( var cu : code._compunits.values() ) { + var funs=new ArrayList(); + for( FunNode fun : code._linker ) + if( fun!=null && fun._compunit==cu ) funs.add(fun); + units.add(new com.seaofnodes.print.IRPrinter.Unit<>(cu._cname,cu._start,cu._stop,funs)); + } + return PRINT.program(code._start,code._stop,units); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java new file mode 100644 index 00000000..9ca0ec51 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -0,0 +1,80 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.graph.GraphAdapter; +import com.seaofnodes.graph.GraphSnapshot.*; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMem; + +/** Chapter 25's view of the IR, including partially constructed parse-time graphs. */ +public class SimpleGraphAdapter extends GraphAdapter { + + @Override protected boolean show(Node n) { + if( n != CodeGen.CODE.ZERO && n != CodeGen.CODE.XCTRL ) return true; + for( int i=0; i0 && n.in(i) instanceof ReturnNode ret + ? ref(ret.fun()) : 0; + } + @Override protected int projectionIndex(Node n) { return n instanceof Proj p ? p.idx() : -1; } + + @Override protected boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i= 2) ) return Role.MEM; + Node def = n.in(i); + if( def instanceof CFGNode ) return Role.CTRL; + if( def != null && isMem(def) ) return Role.MEM; + return Role.DATA; + } + + private boolean isMem(Node n) { + // These Phi subclasses are memory even before their first type computation. + return n instanceof MemPhiNode || n instanceof BulkMemPhiNode || n.isMem() || n._type instanceof TypeMem; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java new file mode 100644 index 00000000..f2ed2f72 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -0,0 +1,35 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.graph.GraphCapture; +import com.seaofnodes.graph.GraphViewer; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.Node; +import java.util.ArrayList; +import java.io.IOException; + +/** Chapter 25's compilation context; capture and event bookkeeping are shared. */ +public class SimpleGraphObserver extends GraphCapture { + private CodeGen _code; + public SimpleGraphObserver() { super(new SimpleGraphAdapter()); } + public static void main(String[] args) throws IOException { + GraphViewer.run(new SimpleGraphObserver()); + } + @Override protected void compile(String src) { + _code = new CodeGen(src); + _code._obs = this; + _code.driver(CodeGen.Phase.TypeCheck); + } + @Override protected String phase() { return _code._phase.name(); } + @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } + @Override protected void detach() { + if( _code != null ) _code._obs = null; + _code = null; + } + @Override protected Node scope() { return _code.P._scope; } + @Override protected void roots(ArrayList roots) { + roots.add(_code._stop); + if( _code._phase == CodeGen.Phase.Parse ) { + roots.addAll(_code.P._xScopes); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/sys.java b/chapter-modules/src/main/java/com/seaofnodes/simple/sys.java new file mode 100644 index 00000000..45106770 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/sys.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + + +/** Default included namespaces for Simple. + + You can think of this as {@code #include } for C or {@code import java.lang.*} for Java. + */ +public abstract class sys { + + public static final String SYS; + + static { + try { + SYS = Files.readString(Path.of("src/main/smp/sys.smp")); + } + catch( Exception e ) { + throw new RuntimeException(e); + } + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/Field.java new file mode 100644 index 00000000..ec4a588d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/Field.java @@ -0,0 +1,142 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.SB; +import java.util.*; + +/** + * Represents a field in a struct. This is not a Type in the type system. + */ +public class Field extends Type { + static final Field[] EMPTY = new Field[0]; + + // The pair {fieldName,type} uniquely identifies a field. + + // Field name + public String _fname; + // Type of the field + public Type _t; + // Unique memory alias, not sensibly part of a "type" but very convenient here. + public int _alias; + // Field is only written in the constructor + public boolean _final; + // Native C storage, outside the containing object's layout. + public boolean _extern; + + private static final Ary FREE = new Ary<>(Field.class); + private Field(String fname, Type type, int alias, boolean xfinal, boolean external ) { super(TFLD); init(fname,type,alias,xfinal,external); } + private Field init(String fname, Type type, int alias, boolean xfinal, boolean external ) { + _fname = fname; + _t = type; + _alias = alias; + _final = xfinal; + _extern = external; + return this; + } + + // Return a filled-in Field; either from free list or alloc new. + private static Field malloc(String fname, Type type, int alias, boolean xfinal, boolean external ) { + return FREE.isEmpty() ? new Field(fname,type,alias,xfinal,external) : FREE.pop().init(fname,type,alias,xfinal,external); + } + // Malloc-from + public Field malloc( ) { return malloc(_fname,null,_alias,_final,_extern); } + @Override Field free(Type t) { + Field f = (Field)t; + assert !f.isFree() && !f._terned; + f._t = null; + f._dual = null; + f._hash = 0; + FREE.push(f); + return this; + } + @Override boolean isFree() { return _t ==null; } + + // Make and intern with listed fields + public static Field make( String fname, Type type, int alias, boolean xfinal ) { + return make(fname,type,alias,xfinal,false); + } + public static Field make( String fname, Type type, int alias, boolean xfinal, boolean external ) { + Field f = malloc(fname,type,alias,xfinal,external); + Field f2 = f.intern(); + if( f2==f ) return f; + return VISIT.isEmpty() ? f2.free(f) : f2.delayFree(f); + } + public Field makeFrom( Type type ) { + return type == _t ? this : make(_fname,type,_alias,_final,_extern); + } + public Field makeFrom( boolean xfinal ) { + return xfinal == _final ? this : make(_fname,_t,_alias,xfinal,_extern); + } + + // Cycle-making-breaking + void setType(Type t) { assert _t ==null; _t = t; } + + public static final Field BOT = make(" ",Type.BOTTOM,-999,false); + public static final Field TEST = make("test",Type.NIL,-2,false); + public static final Field TEST2 = make("test",Type.NIL,-2,true); + public static final Field FLT = make("flt ",TypeFloat.F32,-2,false); + public static final Field EXT = make("test",TypeInteger.I32,-2,false,true); + public static void gather(ArrayList ts) { ts.add(TEST); ts.add(TEST2); ts.add(FLT); ts.add(EXT); } + + @Override Field xmeet( Type that ) { + Field fld = (Field)that; // Invariant + if( this==BOT || fld==BOT ) return BOT; + if( this==BOT.dual() ) return fld ; + if( fld ==BOT.dual() ) return this; + if( _fname!=fld._fname ) { assert !_fname.equals(fld._fname); return BOT; } + assert _alias==fld._alias; + if( _extern!=fld._extern ) return BOT; + return make(_fname, _t.meet(fld._t ), _alias, _final | fld._final,_extern); + } + + @Override + Field xdual() { return malloc(_fname, _t.dual(),_alias,!_final,_extern); } + + @Override Field rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + Field d = malloc(_fname,null,_alias,!_final,_extern); + (_dual = d)._dual = this; // Cross link duals + d._t = _t._terned ? _t.dual() : _t.rdual(); + return d; + } + + @Override boolean _isConstant() { return _t._isConstant(); } + @Override boolean _isFinal() { return _final && _t._isFinal(); } + @Override Field _makeRO() { return _final ? this : make(_fname, _t._makeRO(),_alias,true,_extern); } + @Override Field _close( String name, HashMap TYPES ) { return makeFrom( _t._close(name, TYPES )); } + + @Override Type _upgradeType(HashMap TYPES) { + return makeFrom(_t._upgradeType(TYPES)); + } + + // Override in subclasses + int hash() { return _fname.hashCode() ^ _t.hashCode() ^ _alias ^ (_final ? 1024 : 0) ^ (_extern ? 2048 : 0); } + + private boolean static_eq( Field f ) { + return _fname.equals(f._fname) && _alias==f._alias && _final==f._final && _extern==f._extern && _t !=null && f._t !=null; + } + @Override boolean eq( Type t ) { + Field f = (Field)t; + return static_eq(f) && _t ==f._t; + } + @Override boolean cycle_eq(Type t) { + if( this==t ) return true; + Field f = (Field)t; + return static_eq(f) && _t.cycle_eq(f._t ); + } + + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _t; } + @Override public void set( int idx, Type t ) { _t = t; } + // Tags: final/!final; +alias+name+external-storage flag + + + @Override + public SB _print( SB sb, BitSet visit, boolean html ) { + sb.p(_extern ? "extern C " : "").p(_final?"":"!").p(_fname).p(":").p(_alias).p(" : "); + return _t ==null ? sb.p("---") : _t.print(sb,visit,html); + } + + @Override public String str() { return (_final?"":"!")+_fname; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/Type.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/Type.java new file mode 100644 index 00000000..1979368e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/Type.java @@ -0,0 +1,548 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.*; +import java.util.*; + +/** + * 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 /*implements Cloneable*/ { + // Interning tables + + // Main intern table; a collection of all unique types. + static final HashMap INTERN = new HashMap<>(); + // Starting intern table; used to rapidly reset the main table between tests + private static final HashMap INTERN0= new HashMap<>(); + // Lifetime management during cyclic installs is complex; this is used to + // track created-but-already-interned objects + private static final Ary FREES = new Ary<>(Type.class); + // "visit" bit management, based solely on _uid + static final BitSet BITS = new BitSet(); + + // "visit" bit management. Key varies by kind of visit: + + // - "close" an open cyclic type. Used by the Parser, it will hit on + // same-named structs, even if the structs have different states of + // completion/fields. + // - "uid/name" - Either the UID or name works (unlike "close" which must + // use the name). Used to make cyclic copies with changes, like make + // read-only where there should be only 1 copy of a name in a cycle. + // - "pair of uids" - For recursive MEET, where we need to key off two types + static final HashMap VISIT = new HashMap<>(); + + // Every Type has a UID to allow visit bits on cyclic visits + private static int UID=1; + public char _uid; // Unique ID for every type + + // ---------------------------------------------------------- + // 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 TNIL = 4; // low null of all flavors + static final byte TXNIL = 5; // high or choice null + static final byte TSIMPLE = 5; // End of the Simple Types + + static final byte TSCALAR = 6; // Integer, float, memory pointer or function pointer + static final byte TPTR = 7; // All nil-able pointer values + static final byte TINT = 8; // All Integers; see TypeInteger + static final byte TFLT = 9; // All Floats ; see TypeFloat + static final byte TCONARY =10; // Constant array + static final byte TRPC =11; // Return Program Control (Return PC or RPC) + + static final byte TCYCLIC =12; // Has internal pointers, needs recursive treatment + static final byte TMEMPTR =12; // Memory pointer to a struct type + static final byte TFUNPTR =13; // Function pointer; unique signature and code address (just a bit) + static final byte TMEM =14; // All memory (alias 0) or A slice of memory - with specific alias + static final byte TFLD =15; // Named fields in structs + static final byte TSTRUCT =16; // Structs; tuples with named fields + static final byte TTUPLE =17; // Tuples; finite collections of unrelated Types, kept in parallel + + static final byte TMAX =18; + + // Basic RTTI, useful for a lot of fast tests. + public final byte _type; + public boolean _terned; + + public boolean is_simple() { return _type <= TSIMPLE; } + public boolean is_nokids() { return _type < TCYCLIC; } + private static final String[] STRS = new String[]{"Bot","Top","Ctrl","~Ctrl","null","~nil"}; + protected Type(byte type) { + _type = type; // RTTI + _uid = (char)UID++; // A unique ID for every type + if( UID > 32000 ) + type = type; + assert _uid!=0; // Overflow + } + + public static final Type BOTTOM = new Type( TBOT ).intern(); // ALL + public static final Type TOP = BOTTOM.dual(); + public static final Type CONTROL = new Type( TCTRL ).intern(); // Ctrl + public static final Type XCONTROL = CONTROL.dual(); + public static final Type NIL = new Type( TNIL ).intern(); // low null of all flavors + public static final Type XNIL = NIL.dual(); + public static Type[] gather() { + ArrayList ts = new ArrayList<>(); + ts.add(BOTTOM); + ts.add(CONTROL); + ts.add(NIL); + TypeScalar.gather(ts); + TypeNil.gather(ts); + TypePtr.gather(ts); + TypeInteger.gather(ts); + TypeFloat.gather(ts); + TypeMemPtr.gather(ts); + TypeFunPtr.gather(ts); + TypeMem.gather(ts); + Field.gather(ts); + TypeStruct.gather(ts); + TypeTuple.gather(ts); + TypeRPC.gather(ts); + TypeConAry.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 isHigh () { return _type==TTOP || _type==TXCTRL || _type==TXNIL; } + public boolean isHighOrConst() { return isHigh() || isConstant(); } + + // ---------------------------------------------------------- + + // 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 + // *correctness* with cyclic type definitions. Simple structural recursive + // checks go exponential with merely sharing, but with cycles they will + // stack overflow and crash. Interning means we do not need to have sharing + // checks with every type compare, only during interning. + + // Factory method which interns "this" + @SuppressWarnings("unchecked") + public T intern() { + //assert check(); + assert !_terned; // Do not ask for already-interned + T t2 = (T)INTERN.get(this); + if( t2!=null ) { + assert t2._dual != null; // Prior is complete with dual + assert this != t2; // Do not hashcons twice, should not get self back + return t2; // Return prior + } + // Mid recursive type construction + if( !VISIT.isEmpty() && _type >= TCYCLIC ) { + // No intern attempt at all + return (T)this; + } + // Not in type table + _dual = null; // No dual yet + INTERN.put(this,this); // Put in table without dual + _terned = true; + //Util.hash_quality_check_per(INTERN,"INTERN"); + T d = xdual(); // Compute dual without requiring table lookup, and not setting name + _dual = d; + if( this==d ) return d; // Self-symmetric? Dual is self + assert !equals(d); // Self-symmetric is handled by caller + assert d._dual==null; // Else dual-dual not computed yet + assert INTERN.get(d)==null; + d._dual = (T)this; + INTERN.put(d,d); // Install dual also + d._terned = true; + //assert check(); + return (T)this; + } + + // Recursive visit elsewhere, interns with dual already + T _intern() { + //assert check(); + assert !_terned; + Type t = INTERN.get(this); + if( t!=null ) return (T)t.delayFree(this).delayFree(_dual); + assert !INTERN.containsKey( _dual ); + INTERN.put(this,this); + INTERN.put(_dual,_dual); + _terned = _dual._terned = true; + //assert check(); + return (T)this; + } + + private static boolean check() { + for( Type t : INTERN.keySet() ) { + Type t2 = INTERN.get(t); + assert t==t2; + } + return true; + } + + public static Type find(int uid) { + for( Type t : INTERN.keySet() ) + if( t._uid==uid ) return t; + return null; + } + + + 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 this==t; } + boolean cycle_eq(Type t) { assert is_nokids(); return _type==t._type && eq(t); } + // A pair of uids + int pid( Type that ) { + return _uid < that._uid ? (_uid<<16 | that._uid) : (that._uid<<16 | _uid); + } + + // At/Set child at 'idx' to t + public Type at( int idx ) { throw Utils.TODO("Should not reach here: type has no children"); } + public void set( int idx, Type t ) { throw Utils.TODO("Should not reach here: type has no children"); } + public int nkids() { return 0; } // Number of kids + + + // Clear and re-insert the basic Type INTERN table + public static void reset() { + VISIT.clear(); + FREES.clear(); + if( INTERN0.isEmpty() ) { + // First time run all and collect basic types + Type t = TypeRPC .BOT; // Force class load, intern + Type u = TypeTuple.BOT; // Force class load, intern + Type w = TypePtr .PTR; // Force class load, intern + INTERN0.putAll(INTERN); + } else { + // Later times, reset back to first time + VISIT.put(0,CONTROL); // Defeat assert + for( Iterator it = INTERN.keySet().iterator(); it.hasNext(); ) { + Type t = it.next(); + if( !INTERN0.containsKey(t) ) + { t._terned=false; t.free(t); } + it.remove(); + } + VISIT.clear(); + INTERN.putAll(INTERN0); + } + } + + // ---------------------------------------------------------- + public final Type meet(Type t) { + assert _terned && t._terned; + // 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); + // TypeNil vs TypeNil meet + if( this instanceof TypeNil ptr0 && t instanceof TypeNil ptr1 ) + return ptr0.nmeet(ptr1); + // 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); + // TypeConAry meet its element type is OK + if( this instanceof TypeConAry tcon && t instanceof TypeInteger ti ) return tcon.ymeet(ti); + if( t instanceof TypeConAry tcon && this instanceof TypeInteger ti ) return tcon.ymeet(ti); + // Two distinct scalar families meet at the scalar envelope. + if( this instanceof TypeScalar scalar0 && t instanceof TypeScalar scalar1 ) + return scalar0.smeet(scalar1); + // Mixing 2 unrelated types + return Type.BOTTOM; + } + + // 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; + + // RHS TypeNil vs NIL/XNIL + if( _type==TNIL || _type==TXNIL ) { + if( t instanceof TypeNil ptr ) return _type==TNIL ? ptr.meet0() : ptr.meetX(); + if( t instanceof TypeScalar scalar ) { + if( t._type==TSCALAR ) return t.isHigh() ? this : t; + return TypeScalar.BOT; + } + return t._type==(_type==TNIL ? TXNIL : TNIL) ? TypePtr.PTR : BOTTOM; + } + + // 'this' is only {TCTRL,TXCTRL} + // Other non-simple RHS things bottom out + if( !t.is_simple() ) return BOTTOM; + // If RHS is NIL/XNIL + if( t._type==TNIL || t._type==TXNIL ) return BOTTOM; + // Both are {TCTRL,TXCTRL} and unequal to each other + return _type==TCTRL || t._type==TCTRL ? CONTROL : XCONTROL; + } + + + // The dual is pre-computed on creation and available "for free" thereafter + Type _dual; + public final T dual() { return (T)_dual; } + + T xdual() { + return (T)new Type(switch( _type ) { + case TBOT -> TTOP; + case TTOP -> TBOT; + case TCTRL -> TXCTRL; + case TXCTRL-> TCTRL; + case TNIL -> TXNIL; + case TXNIL -> TNIL; + default -> throw Utils.TODO("Should not reach here: unknown simple type"); + }); + } + + + // ---------------------------------------------------------- + // Our lattice is defined with a MEET and a DUAL. + // JOIN is dual of meet of both duals. + public final Type join(Type t) { + if( this==t ) return this; + return dual().meet(t.dual()).dual(); + } + + // True if this "isa" t; e.g. 17 isa TypeInteger.BOT + public boolean isa( Type t ) { return meet(t)==t; } + + public Type nonZero() { return this==NIL ? TypePtr.XNPTR : TypePtr.NPTR; } + + // Make a zero version of this type, 0 for integers and null for pointers. + public Type makeZero() { return Type.NIL; } + + // Cap at limits + public Type oob() { return isHigh() ? TOP : BOTTOM; } + + // ---------------------------------------------------------- + + // Cyclic types! Flag the start of a cyclic type by putting a sentinel in + // VISIT (and eventually clearing VISIT when done). Then do a normal + // recursive descent visit of all types; this will accumulate types without + // interning them. When done we have to visit the possibly cyclic type and + // intern the whole cycle, possibly hitting the entire cycle on a prior + // interned cycle. Any sub-part might also be interned, including whole + // disjoint cycles. + + // E.g. a new type: "A<->B -> C1<->D1" gets created; the "C1<->D1" cycle + // already exists as "C0<->D0", but the "A<->B" cycle is new. The "A<->B" + // cycle will get interned - except the pointer to C1 needs to get replaced + // with C0. The "C1<->D1" cycle gets freed and "B" will point to the + // "C0<->D0" cycle. + + Type recurOpen() { assert VISIT.isEmpty(); VISIT.put(0L,BOTTOM); return this; } + Type recurClose() { return recurClose(null); } + Type recurClose(Type[] types) { + VISIT.remove(0L); // Just ignore the sentinel + Ary ts = new Ary<>(Type.class); + ts.addAll(VISIT.values()); + if( ts.find(this)== -1 ) ts.add(this); + + // Pass #1: replace already interned fields + assert BITS.isEmpty(); + for( int i=0; i T delayFree(Type free) { + assert !free._terned; + FREES.push(free); + return (T)this; + } + + final Type install() { + if( this instanceof TypeStruct ) { + Type x = _intern(); // Stop the recursion + if( x!=this ) return x.delayFree(this); + } + int nkids = nkids(); + for( int i=0; i TYPES) { + BOTTOM.recurOpen(); + for( TypeStruct t : ts ) + t._close(null, TYPES); + for( int i=0; i TYPES ) { return this; } + public Type widen() { return this; } + + // Replace recursively all TypeBuilders with cyclic TypeStructs + public Type upgradeType(HashMap TYPES) { + return recurOpen()._upgradeType(TYPES).recurClose(); + } + Type _upgradeType(HashMap TYPES) { assert is_nokids(); return this; } + + + // ---------------------------------------------------------- + + // Size in bits to hold an instance of this type. + // Sizes are expected to be between 1 and 64 bits. + // Size 0 means this either takes no space (such as a known-zero field) + // or isn't a scalar to be stored in memory. + public int log_size () { return 3; } // log-size of a type; log_size for a struct is usually undefined + public int size() { return 1< extends TypeScalar { + boolean _any; + byte _widen; + // One of byte or int array + public final A _ary; + + TypeConAry( boolean any, byte widen, A ary ) { super(TCONARY); _any = any; _widen = widen; _ary = ary; } + public static void gather(ArrayList ts) { + ts.add(TypeConAryB.ABC); + ts.add(TypeConAryB.ABCD); + ts.add(TypeConAryI.I123); + } + // Fresh, uninterned type sharing this array; xdual must not intern. + abstract TypeConAry _make(boolean any, byte widen); + + @Override TypeConAry xdual() { return _make( !_any, (byte)(3-_widen) ); } + + @Override Type xmeet(Type t) { + TypeConAry ary = (TypeConAry)t; // Invariant + // Same base array but different? + if( _equals(ary) ) + // Return array with larger widen + return _widen >= ary._widen ? this : ary; + // Unrelated constant arrays, falls to some int range + return elem().meet(ary.elem()); + } + + Type ymeet( TypeInteger ti ) { + // if i can isa *each* element, then maybe can keep. + // no good to i.isa(elem()) because fails the dual + for( int j=0; j { + private TypeConAryB(boolean any, byte widen, byte[] bs) { super(any,widen,bs); } + @Override TypeConAryB _make( boolean any, byte widen ) { return new TypeConAryB(any,widen,_ary); } + public static TypeConAryB make( byte[] bs ) { return new TypeConAryB(false,(byte)0,bs).intern(); } + public static TypeConAryB make( String s ) { return make(s.getBytes()); } + static final TypeConAryB ABC = make("abc"); + static final TypeConAryB ABCD = make("abcd"); + @Override public long at8(int idx) { return _ary[idx]; } + @Override public int len() { return _ary.length; } + @Override public int log_size() { return 0; } + @Override public String str() { return "[\""+new String(_ary)+"\"]"; } + @Override boolean eq(Type t) { return t instanceof TypeConAryB ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); } + @Override int hash() { return Arrays.hashCode(_ary); } + + @Override public void write( BAOS baos ) { baos.write(_ary); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java new file mode 100644 index 00000000..b1b061de --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.type; + +import java.util.Arrays; + +public class TypeConAryI extends TypeConAry { + private TypeConAryI(boolean any, byte widen, int[] bs) { super(any,widen,bs); } + public static TypeConAryI make( int[] bs ) { return new TypeConAryI(false,(byte)0,bs).intern(); } + @Override TypeConAryI _make( boolean any, byte widen ) { return new TypeConAryI(any,widen,_ary); } + static final TypeConAryI I123 = make(new int[]{1,2,3}); + @Override public long at8(int idx) { return _ary[idx]; } + @Override public int len() { return _ary.length; } + @Override public int log_size() { return 2; } + @Override public String str() { return "[i32]"; } + @Override boolean eq(Type t) { return t instanceof TypeConAryI ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); } + @Override int hash() { return Arrays.hashCode(_ary); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFloat.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFloat.java new file mode 100644 index 00000000..8638b8c9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFloat.java @@ -0,0 +1,111 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Float Type + */ +public class TypeFloat extends TypeScalar { + + // - high -64, high -32, con 0, low +32, low +64 + public byte _sz; + + /** + * The constant value or 0 + */ + public double _con; + + private static final Ary FREE = new Ary<>(TypeFloat.class); + private TypeFloat(byte sz, double con) { super(TFLT); init(sz,con); } + private TypeFloat init( byte sz, double con ) { _sz = sz; _con = con; return this; } + public static TypeFloat malloc(byte sz, double con) { return FREE.isEmpty() ? new TypeFloat(sz,con) : FREE.pop().init(sz,con); } + private static TypeFloat make(int sz, double con) { + TypeFloat f = malloc((byte)sz,con); + TypeFloat t2 = f.intern(); + return t2==f ? f : t2.free(f); + } + @Override TypeFloat free(Type t) { + TypeFloat f = (TypeFloat)t; + f._hash = 0; + f._dual=null; + FREE.push(f); + return this; + } + + public static TypeFloat constant(double con) { return make(0, con); } + + public final static TypeFloat ONE = constant(1.0); + public final static TypeFloat FZERO = constant(0.0); + public final static TypeFloat F32 = make(32, 0); + public final static TypeFloat F64 = make(64, 0); + + public static void gather(ArrayList ts) { ts.add(F64); ts.add(F32); ts.add(constant(3.141592653589793)); } + + @Override public String str() { + return switch( _sz ) { + case -64 -> "~flt"; + case -32 -> "~f32"; + case 0 -> ""+_con+((float)_con==_con ? "f" : ""); + case 32 -> "f32"; + case 64 -> "flt"; + default -> throw Utils.TODO("Should not reach here: unknown float size"); + }; + } + private boolean isF32() { return ((float)_con)==_con; } + + @Override public boolean isHigh () { return _sz< 0; } + @Override public int log_size() { return _sz==32 || _sz==-32 ? 2 : 3; } + + public double value() { return _con; } + + @Override + public TypeFloat xmeet(Type other) { + TypeFloat f = (TypeFloat)other; + // Invariant from caller: 'this' != 'other' and same class (TypeFloat). + TypeFloat i = (TypeFloat)other; // Contract + // Larger size in i1, smaller in i0 + TypeFloat i0 = _sz < i._sz ? this : i; + TypeFloat i1 = _sz < i._sz ? i : this; + + if( i1._sz== 64 ) return F64; + if( i0._sz==-64 ) return i1; + if( i1._sz== 32 ) + return i0._sz==0 && !i0.isF32() ? F64 : F32; + if( i1._sz!= 0 ) return i1; + // i1 is a constant + if( i0._sz==-32 ) + return i1.isF32() ? i1 : F64; + // Since both are constants, and are never equals (contract) unequals + // constants fall to bottom + return i0.isF32() && i1.isF32() ? F32 : F64; + } + + @Override + TypeFloat xdual() { + return _isConstant() ? this : new TypeFloat((byte)-_sz,0); // Constants are a self-dual + } + + @Override boolean _isConstant() { return _sz==0; } + + @Override Type _makeStorage() { + if( _isConstant() ) return isF32() ? F32 : F64; + return isHigh() ? dual() : this; + } + + @Override public Type makeZero() { return FZERO; } + + + @Override + int hash() { return (int)(Double.hashCode(_con) ^ _sz ^ (1<<17)); } + @Override + public boolean eq( Type t ) { + TypeFloat i = (TypeFloat)t; // Contract + // Allow NaN to check for equality + return _sz==i._sz && Double.doubleToLongBits(_con)==Double.doubleToLongBits(i._con); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java new file mode 100644 index 00000000..301fcc8b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java @@ -0,0 +1,313 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.*; +import java.util.*; + +/** + * Represents a Pointer to a function. + *

+ * Functions have argument types and a return type, making a function + * signature. Within a signature there can be many instances of functions, and + * each function is labeled with a small integer constant. A TFP can represent + * a single signature and a set of functions; the set might contain only 1 + * function or the zeroth function (the null ptr). + *

+ * Functions with different signatures cannot be mixed, and will result in a + * bottom function type, which can only be null-checked. + */ +public class TypeFunPtr extends TypeNil { + // A TypeFunPtr is Signature and a set of functions. + public Type[] _sig; + public Type _ret; + boolean _open; // Extra args are true:scalar BOT, false:scalar TOP + // Set of functions. Bit zero is used for mutable/immutable safety. Sign + // bit repeats infinitely. + public int[] _fidxs; + + private static final Ary FREE = new Ary<>(TypeFunPtr.class); + private TypeFunPtr(byte nil) { super(TFUNPTR,nil); } + private TypeFunPtr init(byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + assert sig != null; + _nil = nil; + _open = open; + _sig = sig; + _ret = ret; + assert _fidxs == null; + _fidxs= XInt.intern(fidxs); + return this; + } + static TypeFunPtr malloc( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + return (FREE.isEmpty() ? new TypeFunPtr(nil) : FREE.pop()).init(nil,open,sig,ret,fidxs); + } + @Override TypeFunPtr free(Type t) { + TypeFunPtr fun = (TypeFunPtr)t; + assert !fun.isFree() && !fun._terned; + fun._sig = null; + fun._ret = null; + fun._dual = null; + fun._hash = 0; + fun._fidxs = null; + FREE.push(fun); + return this; + } + @Override boolean isFree() { return _sig==null; } + + // All fields directly listed + public static TypeFunPtr make( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + TypeFunPtr fun = malloc(nil,open,sig,ret,fidxs); + TypeFunPtr f2 = fun.intern(); + if( f2==fun ) return fun; + return VISIT.isEmpty() ? f2.free(fun) : f2.delayFree(fun); + } + // Trim trailing args which match the default. + private static TypeFunPtr makeAdjusted( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + int nargs = sig.length; + while( nargs > 0 && sig[nargs-1] == def ) + nargs--; + if( nargs != sig.length ) + sig = nargs == 0 ? ARG_EMPTY : Arrays.copyOf(sig,nargs); + return make(nil,open,sig,ret,fidxs); + } + public static TypeFunPtr make( boolean nil, Type[] sig, Type ret ) { return makeAdjusted((byte)(nil ? 3 : 2),true,sig,ret, XInt.FULL); } + public static TypeFunPtr make1( byte nil, boolean open, Type[] sig, Type ret, int fidx ) { + return makeAdjusted(nil,open,sig,ret,XInt.make(fidx)); + } + + + @Override TypeFunPtr makeFrom( byte nil ) { return nil ==_nil ? this : make( nil,_open,_sig,_ret, _fidxs); } + public TypeFunPtr makeFrom( Type ret ) { return ret ==_ret ? this : make( _nil,_open,_sig, ret, _fidxs); } + public TypeFunPtr makeFrom( int fidx ) { return make1(_nil,_open,_sig,_ret,fidx); } + public TypeFunPtr makeFrom( Type arg, int idx ) { + // Alter named arg + Type[] sig = _sig.clone(); + sig[idx] = arg; + return make(_nil,_open,sig,_ret,_fidxs); + } + @Override public Type nonZero() { return makeFrom((byte)(_nil <= 1 ? 1 : 2)); } + + public static final Type[] TEMPTY = new Type[0]; + static final Type[] TINT = new Type[]{TypeInteger.BOT}; + static final Type[] TINTMEM = new Type[]{TypeInteger.BOT,TypeFloat.F32}; + static final Type[] TINTINT = new Type[]{TypeInteger.BOT,TypeInteger.BOT}; + public static TypeFunPtr BOT = make((byte)3,true,TEMPTY,Type.BOTTOM, XInt.FULL); + public static TypeFunPtr TEST = make((byte)2,true,TINTMEM,TypeInteger.BOT, XInt.SET1); + public static TypeFunPtr TEST0 = make((byte)3,true,TINTMEM,TypeInteger.BOT, XInt.SET3); + public static TypeFunPtr MAIN = make((byte)3,true,TINT ,Type.BOTTOM, XInt.FULL); // Main can return anything + public static TypeFunPtr CALLOC= make((byte)3,true,TINTINT,TypeMemPtr .BOT, XInt.FULL); + public static void gather(ArrayList ts) { + ts.add(TEST); + ts.add(TEST0); + ts.add(BOT); + ts.add(MAIN); + ts.add(CALLOC); + ts.add(make((byte)2,false,TINT,TypeInteger.I32, XInt.SET1)); + ts.add(make((byte)2,true ,TINT,TypeInteger.BOT, XInt.SET3)); + ts.add(make1((byte)2,true, + new Type[]{TypeInteger.BOT}, + TypeInteger.BOT, 65)); + ts.add(make1((byte)2,true, + new Type[]{TypeInteger.BOT, TypeScalar.BOT}, + TypeInteger.BOT, 65)); + // Explicit trailing defaults must agree with the implicit argument tail. + for( boolean open : new boolean[]{false,true} ) { + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + ts.add(make1((byte)2,open,TINT,TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{TypeInteger.BOT,def,def},TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{def,def},TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{def,TypeInteger.BOT},TypeInteger.I32,1)); + } + } + + private static final Type[] ARG_EMPTY = new Type[0]; + @Override + Type xmeet(Type t) { + TypeFunPtr that = (TypeFunPtr) t; + boolean open = _open | that._open; + Type def0 = this._open ? TypeScalar.BOT : TypeScalar.TOP; + Type def1 = that._open ? TypeScalar.BOT : TypeScalar.TOP; + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + + int nargs = Math.max(_sig.length,that._sig.length); + Type[] args = new Type[nargs]; + + for( int i=0; i0 && args[nargs-1] == def ) + nargs--; + + if( nargs != args.length ) + args = nargs == 0 ? ARG_EMPTY : Arrays.copyOf(args,nargs); + + return make(xmeet0(that), open, args, _ret.meet(that._ret), XInt.meet(_fidxs,that._fidxs)); + } + + static Type[] xmeet( Type[] ts0, Type[] ts1) { + if( ts0==ts1 ) return ts0; + assert ts0.length==ts1.length; + Type[] ts = new Type[ts0.length]; + for( int i=0; i TYPES ) { + Type[] sig = new Type[_sig.length]; + TypeFunPtr fun = malloc(_nil,true,sig,null,_fidxs); + // Now start the recursion + fun._ret = _ret._close(name, TYPES ); + for( int i=0; i TYPES) { + Type[] sig = new Type[_sig.length]; + TypeFunPtr fun = malloc(_nil,_open,sig,null,_fidxs); + // Now start the recursion + fun._ret = _ret._upgradeType(TYPES); + for( int i=0; i "),visit,html).p(" #"); + // Print fidxs + return sb.p(printFIDX()).p("}").p(q()); + } + String printFIDX() { + String tilde = isHigh() ? "~" : ""; + String fidx = XInt.str(_fidxs); + return tilde+fidx; + } + + // Usage: for( long fidxs=fidxs(); fidxs!=0; fidxs=nextFIDX(fidxs) ) { int fidx = Long.numberOfTrailingZeros(fidxs); ... } + //public static long nextFIDX(long fidxs) { return fidxs & (fidxs-1); } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeInteger.java new file mode 100644 index 00000000..a2cf08b0 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -0,0 +1,185 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Integer Type + */ +public class TypeInteger extends TypeScalar { + /** + * Describes an integer *range* - everything from min to max; both min and + * max are inclusive. If min==max, this is a constant. + *
+ * If min <= max, this is a below center (towards bottom). + * If min > max, this is an above center (towards top). + */ + public long _min, _max; + + public byte _widen; + + private static final Ary FREE = new Ary<>(TypeInteger.class); + private TypeInteger(long min, long max, byte widen) { super(TINT); init(min,max,widen); } + private TypeInteger init(long min, long max, byte widen) { + _min = min; _max = max; _widen=widen; + return this; + } + public static TypeInteger malloc(long lo, long hi, byte widen) { return FREE.isEmpty() ? new TypeInteger(lo,hi,widen) : FREE.pop().init(lo,hi,widen); } + public static TypeInteger make(long lo, long hi) { return make(lo,hi,(byte)0); } + public static TypeInteger make(long lo, long hi, byte widen) { + TypeInteger i = malloc(lo,hi,widen); + TypeInteger t2 = i.intern(); + return t2==i ? i : t2.free(i); + } + public TypeInteger makeWide(byte widen) { + if( widen == _widen ) return this; + if( _min == Long.MIN_VALUE && _max == Long.MAX_VALUE ) + return TypeInteger.BOT; + if( _min==_max ) return this; // Leave constants always narrow + return make(_min,_max,widen); + } + @Override TypeInteger free(Type t) { + TypeInteger i = (TypeInteger)t; + i._min = i._max = 0; + i._widen = 0; + i._hash = 0; + i._dual = null; + FREE.push(i); + return this; + } + + public Type same_but_slightly_wider_than( ) { + return _widen < 3 ? make(_min,_max, (byte)(_widen+1)) : BOT; + } + // Force max widen + @Override public Type widen() { + return _widen==3 ? this : make(_min,_max,(byte)3); + } + public static TypeInteger constant(long con) { return make(con, con); } + + public final static TypeInteger ZERO= make(0,0); + public final static TypeInteger FALSE=ZERO; + public final static TypeInteger TRUE= make(1,1); + public final static TypeInteger TWO = make(2,2); // Shows up in some Simple tests as the starting argument + + public final static TypeInteger I1 = make(-1,0,(byte)3); + public final static TypeInteger I8 = make(-128,127,(byte)3); + public final static TypeInteger I16 = make(-32768,32767,(byte)3); + public final static TypeInteger I32 = make(-1L<<31,(1L<<31)-1,(byte)3); + public final static TypeInteger BOT = make(Long.MIN_VALUE,Long.MAX_VALUE,(byte)3); + public final static TypeInteger TOP = (TypeInteger)BOT.dual(); + + public final static TypeInteger U1 = make(0,1,(byte)3); + public final static TypeInteger BOOL= U1; + public final static TypeInteger U8 = make(0,255,(byte)3); + public final static TypeInteger U16 = make(0,65535,(byte)3); + public final static TypeInteger U32 = make(0,(1L<<32)-1,(byte)3); + + public final static TypeInteger FATBOOL = make(0,1,(byte)2); + + public static void gather(ArrayList ts) { + ts.add(I32); ts.add(BOT); ts.add(U1); ts.add(I1); ts.add(U8); ts.add(ZERO); ts.add(FATBOOL); + // Ranges before and after excluding zero, at each widening level. + for( byte widen=0; widen<=3; widen++ ) { + ts.add(make(0,255,widen)); + ts.add(make(1,255,widen)); + ts.add(make(-255,0,widen)); + ts.add(make(-255,-1,widen)); + } + } + + @Override public String str() { + if( _isConstant() ) return ""+_min; + long lo = _min, hi = _max; + String x = ""; + if( hi < lo ) { + lo = _max; hi = _min; + x = "~"; + } + return x+_str(lo,hi); + } + private static String _str(long lo, long hi) { + if( lo==Long.MIN_VALUE && hi==Long.MAX_VALUE ) return "i64"; + if( lo== 0 && hi== 1 ) return "bool"; + if( lo== -1 && hi== 0 ) return "i1"; + if( lo== -128 && hi== 127 ) return "i8"; + if( lo== -32768 && hi== 32767 ) return "i16"; + if( lo== -1L<<31 && hi==(1L<<31)-1 ) return "i32"; + if( lo== 0 && hi== 255 ) return "u8"; + if( lo== 0 && hi== 65535 ) return "u16"; + if( lo== 0 && hi==(1L<<32)-1 ) return "u32"; + return "["+lo+"-"+hi+"]"; + } + + @Override public boolean isHigh () { return _min > _max; } + + @Override public int log_size() { + if( isHigh() ) return 0; // High types are dead, and should never hit code emission. + if( this==I8 || this==U8 || this==BOOL ) return 0; // 1<<0 == 1 bytes + if( this==I16 || this==U16 ) return 1; // 1<<1 == 2 bytes + if( this==I32 || this==U32 ) return 2; // 1<<2 == 4 bytes + if( this==BOT ) return 3; // 1<<3 == 8 bytes + if( _isConstant() ) return log(_min); // just const here + int lo = log(_min); + int hi = log(_max); + return Math.max(lo,hi); + } + + private static int log(long x) { + if( -0xFFL <= x && x <= 0xFFL) return 0; + if( -0xFFFFL <= x && x <= 0xFFFFL) return 1; + if(-0xFFFFFFFFL <= x && x <= 0xFFFFFFFFL) return 2; + return 3; + } + + public long value() { assert _isConstant(); return _min; } + + // AND-mask of forced zeros. e.g. unsigned types will return their mask; + // u8 will return 0xFF. But also a range of 16-18 (0x10-0x12) will return + // 0x13 - no value in the range {16,17,18} will allow bit 0x04 to be set. + public long mask() { + if( isHigh() ) return 0; + if( isConstant() ) return _min; + // Those bit positions which differ min to max + long x = _min ^ _max; + // Highest '1' bit in the differ set. Since the range is from min to + // max, all values below here are possible. + long ff1 = Long.highestOneBit(x); + // Make a all-1's mask from ff1, and set over the same bits (either min + // or max is ok). + long mask = _min | (ff1-1) | ff1; + return mask; + } + + @Override + public Type xmeet(Type other) { + // Invariant from caller: 'this' != 'other' and same class (TypeInteger) + TypeInteger i = (TypeInteger)other; // Contract + return make(Math.min(_min,i._min), Math.max(_max,i._max), (byte)Math.max(_widen,i._widen)); + } + + @Override TypeInteger xdual() { return malloc(_max,_min,(byte)(3-_widen)); } + + @Override public TypeInteger nonZero() { + if( isHigh() ) return this; + if( this==ZERO ) return null; // No sane answer + if( _min==0 ) return make(1,Math.max(_max,1),_widen); // specifically good on BOOL + if( _max==0 ) return make(_min,-1,_widen); + return this; + } + @Override public Type makeZero() { return ZERO; } + @Override boolean _isConstant() { return _min == _max; } + @Override Type _makeStorage() { + if( _isConstant() ) return BOT; + return isHigh() ? dual() : this; + } + + + @Override int hash() { return Utils.fold(_min) * Utils.fold(_max) + _widen; } + @Override public boolean eq( Type t ) { + return t instanceof TypeInteger i && _min==i._min && _max==i._max && _widen==i._widen; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMem.java new file mode 100644 index 00000000..4234a5e5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMem.java @@ -0,0 +1,229 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.*; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; + + +/** + * Represents a slice of memory corresponding to a set of aliases + */ +public class TypeMem extends Type { + + // Which slice of memory? + // 1 and TOP means no slice. + // 1 and BOT means all memory. + // N means alias slice#N. + public int _alias; + public Type _t; // Memory contents, some scalar type + public boolean _one; // Singleton memory, corresponding to TypeMemPtr._one. + public boolean _clz; // Class-object singleton memory; true singletons stay constant after construction. + public boolean _final;// Memory is only set in the constructor + + // Escaped aliases and functions - escaped into this memory. These are + // reachable from some chain of indirections from `_t`. Bit zero is + // used for mutable/immutable safety. Sign bit repeats infinitely. + public int[] _escFs; + public int[] _escAs; + + static public int CNT; + + private static final Ary FREE = new Ary<>(TypeMem.class); + + private TypeMem() { super(TMEM); CNT++; } + private TypeMem init(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { + _alias = alias; + _t = t; + _one = one; + _clz = clz; + // Public bulk memory is an aggregate of otherwise-unsplit aliases; + // a single final bit cannot describe it. Keep the lattice-dual high + // form distinct, and retain finality for private singleton memory and + // precise alias slices. + _final = t!=null && alias==1 && !one && !t.isHigh() ? false : xfinal; + + assert _escFs == null && _escAs == null; + _escFs = XInt.intern(fidxs); + _escAs = XInt.intern(aliases); + return this; + } + @Override TypeMem free(Type t) { + TypeMem mem = (TypeMem)t; + mem._alias= -99; + mem._dual = null; + mem._hash = 0; + mem._t = null; + mem._one = false; + mem._clz = false; + mem._final = false; + mem._escFs = null; + mem._escAs = null; + FREE.push(mem); + return this; + } + @Override boolean isFree() { return _alias == -99; } + + public static TypeMem malloc(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { + return (FREE.isEmpty() ? new TypeMem() : FREE.pop()).init(alias,t,one,clz,xfinal,fidxs,aliases); + } + public static TypeMem make(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { + if( fidxs ==null ) fidxs = XInt.EMPTY; + if( aliases==null ) aliases= XInt.EMPTY; + TypeMem f = malloc(alias,t,one,clz,xfinal, fidxs, aliases); + TypeMem f2 = f.intern(); + if( f2==f ) return f; + return VISIT.isEmpty() ? f2.free(f) : f2.delayFree(f); + } + public static TypeMem make(int alias, Type t) { return make(alias,t,false,false,false,null,null); } + // Make a private memory + public static TypeMem makePrivate(Type t) { return make(1,t,true,false,false,escapeFIDX(XInt.EMPTY,t), escapeAlias(XInt.EMPTY,t)); } + + public TypeMem makeFrom(int alias) { return make(alias,_t,_one,_clz,_final,_escFs,_escAs); } + public TypeMem makeFrom(int[] fidxes, int[] aliases) { + return make(_alias,_t,_one,_clz,_final,fidxes,aliases); + } + // Existing escapes plus `t` escapes + public TypeMem escapes(Type t) { + return make(_alias,_t,_one,_clz,_final, escapeFIDX(_escFs,t), escapeAlias(_escAs,t)); + } + // Existing escapes plus `t` escapes + public TypeMem escapesFrom(Type t) { + return make(_alias,_t,_one,_clz,_final, escapeFIDX(_escFs,t), escapeAlias(_escAs,t)); + } + public TypeMem escapesAliases(Type t) { + return make(_alias,_t,_one,_clz,_final, _escFs, escapeAlias(_escAs,t)); + } + + public static final TypeMem TOP = make(1, Type.TOP ,true ,true, true , XInt.EMPTY, XInt.EMPTY); + public static final TypeMem BOT = make(1, Type.BOTTOM,false,false,true , XInt.FULL , XInt.FULL ); + public static final TypeMem START= make(1,Type.BOTTOM,false,false,false, XInt.EMPTY, XInt.EMPTY); + public static final TypeMem SELF_MEM = make(1, TypeStruct.BOT); + + public static void gather(ArrayList ts) { + ts.add(make(2,Type.NIL)); + ts.add(make(2,TypeInteger.ZERO)); + ts.add(BOT); + ts.add(SELF_MEM); + ts.add(START); + ts.add(make(2,TypeInteger.ZERO,true,false,true,XInt.EMPTY,XInt.make(2))); + ts.add(make(2,TypeInteger.BOT,false,false,false,XInt.EMPTY,XInt.FULL)); + } + + @Override + TypeMem xmeet(Type t) { + TypeMem that = (TypeMem) t; // Invariant: TypeMem and unequal + if( this==TOP ) return that; + if( that==TOP ) return this; + if( this==BOT ) return BOT; + if( that==BOT ) return BOT; + // Treat a alias-1 has either very-high or very-low, according to "_t" + int alias = _alias==that._alias ? _alias // Same-same + : this._alias==1 && this._t.isHigh() ? that._alias // this is high-1 alias, so take that + : that._alias==1 && that._t.isHigh() ? this._alias // that is high-1 alias, so take this + : 1; + Type mt = _t.meet(that._t); + int[] fidxs = XInt.meet( _escFs, that._escFs ); + int[] aliases= XInt.meet( _escAs, that._escAs ); + // Singleton and class facts are kept only when both sides agree. + // Finality is a low-side fact, and flips on the dual side. + boolean highAlias1 = (this._alias==1 && this._t.isHigh()) || (that._alias==1 && that._t.isHigh()); + boolean xfinal = highAlias1 ? (_final & that._final) : (_final | that._final); + return make(alias, mt, _one & that._one, _clz & that._clz, xfinal, fidxs, aliases); + } + + @Override + Type xdual() { return malloc(_alias,_t.dual(),!_one,!_clz,!_final, XInt.dual(_escFs), XInt.dual(_escAs)); } + + @Override TypeMem rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + TypeMem d = malloc(_alias,null,!_one,!_clz,!_final, XInt.dual(_escFs), XInt.dual(_escAs)); + (_dual = d)._dual = this; // Cross link duals + d._t = _t._terned ? _t.dual() : _t.rdual(); + if( d._alias==1 && !d._one ) { assert !d._t.isHigh(); d._final = false; } + return d; + } + + @Override public TypeMem makeZero() { return TOP; } + @Override public boolean isHigh() { return _t.isHigh(); } + @Override boolean _isConstant() { return _one && _clz && _alias!= 1 && _t._isConstant(); } + @Override public int log_size() { throw Utils.TODO("Should not reach here: memory has no scalar storage size"); } + @Override boolean _isFinal() { throw Utils.TODO("Should not reach here"); } + @Override TypeMem _makeStorage() { throw Utils.TODO("Should not reach here"); /*return make(_alias,_t._makeStorage(),_one,_clz,_final,_escFs,_escAs);*/ } + + @Override TypeMem _close( String name, HashMap TYPES ) { return malloc(_alias,_t._close(name, TYPES ),_one,_clz,_final,_escFs,_escAs); } + + @Override Type _upgradeType(HashMap TYPES) { + return make(_alias,_t._upgradeType(TYPES),_one,_clz,_final,_escFs,_escAs); + } + + @Override int hash() { return 9876543 + _alias + _t.hashCode() + (_one ? 8196 : 0) + (_clz ? 4098 : 0) + (_final ? 16392 : 0) + XInt.hash(_escFs) + XInt.hash(_escAs); } + + @Override boolean eq(Type t) { + TypeMem that = (TypeMem) t; // Invariant + return _alias == that._alias && _t == that._t && _one == that._one && _clz == that._clz && _final == that._final && _escFs == that._escFs && _escAs == that._escAs; + } + + @Override boolean cycle_eq(Type t) { + TypeMem that = (TypeMem) t; // Invariant + return _alias == that._alias && _one == that._one && _clz == that._clz && _final == that._final && _escFs == that._escFs && _escAs == that._escAs && _t.cycle_eq(that._t); + } + + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _t; } + @Override public void set( int idx, Type t ) { _t = t; } + + + @Override public SB _print(SB sb, BitSet visit, boolean html) { + if( this==BOT ) return sb.p("MemBot"); + if( this==TOP ) return sb.p("MemTop"); + sb.p('#'); + if( !_final ) sb.p('!'); // Mutable memory + if( _one ) sb.p(_clz ? 'z' : '-'); + if( _alias==0 ) return sb.p(_t._type==TTOP ? "TOP" : "BOT"); + sb.p(_alias).p(":["); + _t.print(sb,visit,html).p(","); + XInt.str(sb,_escFs).p(","); + return XInt.str(sb,_escAs).p("]"); + } + + @Override public String str() { + if( this==BOT ) return "MemBot"; + if( this==TOP ) return "MemTop"; + SB sb = new SB().p('#'); + if( !_final ) sb.p('!'); // Mutable memory + if( _one ) sb.p(_clz ? 'z' : '-'); + if( _alias==0 ) return sb.p(_t._type==TTOP ? "TOP" : "BOT").toString(); + sb.p(_alias).p(":[").p(_t.str()).p(","); + XInt.str(sb,_escFs).p(","); + return XInt.str(sb,_escAs).p("]").toString(); + } + + + // Tracking bulk escaped aliases and fidxs + + // Escape all FIDXs from t + private static int[] escapeFIDX( int[] xs, Type t ) { + return switch(t) { + case TypeMemPtr tmp -> escapeFIDX(xs,tmp._obj); + case TypeStruct ts -> ts._open && !ts.isAry() + ? XInt.FULL // Open structs can have any fields + : XInt.meet(xs,ts.fidxs()); + case TypeFunPtr tfp -> XInt.meet(xs,tfp.fidxs()); + case TypeMem mem -> XInt.meet(xs,mem._escFs); + case Type tt -> tt == BOTTOM ? XInt.FULL : xs; + }; + } + + private static int[] escapeAlias( int[] xs, Type t ) { + return switch(t) { + case TypeMemPtr tmp -> escapeAlias(xs,tmp._obj); + case TypeStruct ts -> ts._open && !ts.isAry() + ? XInt.FULL // Open structs can have any fields + : XInt.meet(xs,ts.aliases()); + case TypeMem mem -> XInt.meet(xs,mem._escAs); + case Type tt -> tt == BOTTOM ? XInt.FULL : xs; + }; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java new file mode 100644 index 00000000..73ca6190 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java @@ -0,0 +1,179 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.SB; +import java.util.*; + +/** + * Represents a Pointer to memory. + * + * Null is generic pointer to non-existent memory. + * *void is a non-Null pointer to all possible refs, both structs and arrays. + * Pointers can be to specific struct and array 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 TypeNil { + // A TypeMemPtr is pair (obj,nil) + // where obj is a TypeStruct, possibly TypeStruct.BOT/TOP + // where nil is an explicit null is allowed or not + + // Examples: + // (Person,false) - a not-nil Person + // (Person,true ) - a Person or a nil + // (BOT ,false) - a not-nil void* (unspecified struct) + // (TOP ,true ) - a nil + + public TypeStruct _obj; + // Deep read-only access through this pointer; independent of slot finality. + public boolean _ro; + + public boolean _one; // Singleton pointer value; the pointer itself is constant. + public boolean _pub; // Escaped/public pointer. False means constructor-private. + + private static final Ary FREE = new Ary<>(TypeMemPtr.class); + private TypeMemPtr(byte nil, TypeStruct obj, boolean one, boolean pub) { super(TMEMPTR,nil); init(nil,obj,one,pub); } + private TypeMemPtr init(byte nil, TypeStruct obj, boolean one, boolean pub) { _nil=nil; _obj=obj; _ro=obj==TypeStruct.BOT; _one=one; _pub=pub; return this; } + // Return a filled-in TypeMemPtr; either from free list or alloc new. + static TypeMemPtr malloc(byte nil, TypeStruct obj, boolean one, boolean pub) { + return FREE.isEmpty() ? new TypeMemPtr(nil,obj,one,pub) : FREE.pop().init(nil,obj,one,pub); + } + + // All fields directly listed + public static TypeMemPtr make(byte nil, TypeStruct obj, boolean one) { + return make(nil,obj,one,true); + } + public static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub) { + return make(nil,obj,one, pub,obj==TypeStruct.BOT); + } + private static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub, boolean ro) { + TypeMemPtr tmp = malloc(nil, obj, one, pub).access(ro); + TypeMemPtr t2 = tmp.intern(); + if( t2==tmp ) return tmp; + return VISIT.isEmpty() ? t2.free(tmp) : t2.delayFree(tmp); + } + @Override TypeMemPtr free(Type t) { + TypeMemPtr tmp = (TypeMemPtr)t; + tmp._nil = -99; + tmp._obj = null; + tmp._one = false; + tmp._pub = true; + tmp._dual = null; + tmp._hash = 0; + FREE.push(tmp); + return this; + } + @Override boolean isFree() { return _obj==null; } + + static TypeMemPtr make(byte nil, TypeStruct obj) { return make(nil, obj, false); } + public static TypeMemPtr make (TypeStruct obj) { return make((byte)2, obj); } + public static TypeMemPtr makeNullable(TypeStruct obj) { return make((byte)3, obj); } + public static TypeMemPtr makePrivate (TypeStruct obj) { return make((byte)2, obj, false, false); } + + public TypeMemPtr makeFrom(TypeStruct obj) { return obj==_obj ? this : make(_nil, obj, _one, _pub,_ro); } + public TypeMemPtr makeNullable() { return makeFrom((byte)3); } + /** Inferred mutable local: nullable, public and non-singleton. */ + public TypeMemPtr makeVar() { return (TypeMemPtr)makeStorage(); } + /** Public-memory shape: nullable and non-singleton, preserving the referent. */ + @Override TypeMemPtr _makeStorage() { return make(_nil,_obj._makeStorage(),false,true,_ro); } + @Override TypeMemPtr makeFrom(byte nil) { return nil==_nil ? this : make(nil, _obj, _one, _pub,_ro); } + @Override public Type nonZero() { return makeFrom((byte)(_nil <= 1 ? 1 : 2)); } + + private TypeMemPtr access(boolean ro) { _ro=ro; return this; } + + public TypeMemPtr withAccess(boolean ro) { + if( _ro==ro ) return this; + TypeMemPtr ptr = malloc(_nil, _obj, _one, _pub); + ptr._ro = ro; + TypeMemPtr res = ptr.intern(); + if( res==ptr ) return ptr; + return VISIT.isEmpty() ? res.free(ptr) : res.delayFree(ptr); + } + + // Writable slots can load and store references, so their element access + // must agree in both directions. A read-only outer view can weaken access. + @Override public boolean accessISA(Type dst) { + if( !(dst instanceof TypeMemPtr ptr) ) return true; + if( _ro && !ptr._ro ) return false; + if( ptr._ro || _obj._fields==null || ptr._obj._fields==null || !_obj.isAry() || !ptr._obj.isAry() ) return true; + Type a = _obj._fields[1]._t, b = ptr._obj._fields[1]._t; + return a.accessISA(b) && b.accessISA(a); + } + + // An abstract pointer, pointing to either a Struct or an Array. + // Can also be null or not, so 4 choices {TOP,BOT} x {nil,not} + public static final TypeMemPtr BOT = make((byte)3, TypeStruct.BOT); + public static final TypeMemPtr TOP = BOT.dual(); + public static final TypeMemPtr NOTBOT = make((byte)2,TypeStruct.BOT); + + public static final TypeMemPtr TEST= make((byte)2, TypeStruct.TEST); + public static void gather(ArrayList ts) { ts.add(NOTBOT); ts.add(BOT); ts.add(TEST); ts.add(TEST.makeRO()); } + + @Override + public TypeNil xmeet(Type t) { + TypeMemPtr that = (TypeMemPtr) t; + assert !isFree() && !that.isFree(); + // Can't keep _one if mixing two unequal singletons; the result is not + // either singleton. Once either side is public, the meet is public. + boolean one = _one && that._one && (_obj==that._obj || !(_obj.isConAry() || that._obj.isConAry())); + return make(xmeet0(that), (TypeStruct)_obj.meet(that._obj), one, _pub | that._pub,_ro | that._ro); + } + + @Override + TypeMemPtr xdual() { return malloc( dual0(), _obj.dual(), !_one, !_pub).access(!_ro); } + + @Override TypeMemPtr rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + TypeMemPtr d = malloc(dual0(), null, !_one, !_pub).access(!_ro); + (_dual = d)._dual = this; // Cross link duals + d._obj = _obj._terned ? _obj.dual() : _obj.rdual(); + return d; + } + + // RHS is NIL; do not deep-dual when crossing the center line + @Override Type meet0() { return _nil==3 ? this : make((byte)3,_obj,false,true,_ro); } + + @Override boolean _isConstant() { return _one; } + @Override boolean _isFinal() { return _ro; } + @Override TypeMemPtr _makeRO() { return withAccess(true); } + @Override TypeMemPtr _close( String name, HashMap TYPES ) { return malloc(_nil,_obj._close(name, TYPES ),_one,_pub).access(_ro); } + + @Override Type _upgradeType(HashMap TYPES) { + return makeFrom((TypeStruct)_obj._upgradeType(TYPES)); + } + + @Override public int log_size() { return 3; } // (1<<3)==8-byte pointers + + @Override int hash() { return (_ro ? 8192 : 0) ^ _obj.hashCode() ^ super.hash() ^ (_one ? 2048 : 0) ^ (_pub ? 4096 : 0); } + + @Override boolean eq(Type t) { + TypeMemPtr ptr = (TypeMemPtr)t; // Invariant + return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj == ptr._obj; + } + @Override boolean cycle_eq(Type t) { + if( t._type != TMEMPTR ) return false; + TypeMemPtr ptr = (TypeMemPtr)t; // Invariant + return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj.cycle_eq(ptr._obj); + } + + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _obj; } + @Override public void set( int idx, Type t ) { _obj = (TypeStruct)t; } + + + @Override public String str() { + if( this==BOT ) return "PtrBot"; + if( this==TOP ) return "PtrTop"; + if( this== NOTBOT) return "*void"; + return x()+"*"+(_obj==null?"---":_obj.str())+q(); + } + + @Override SB _print(SB sb, BitSet visit, boolean html ) { + if( this==BOT ) return sb.p("PtrBot"); + if( this==TOP ) return sb.p("PtrTop"); + if( this== NOTBOT) return sb.p("*void"); + return _obj.print(sb.p(x()).p("*"),visit,html).p(q()); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeNil.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeNil.java new file mode 100644 index 00000000..2c1c1d76 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeNil.java @@ -0,0 +1,61 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; + +/** + * Nil-able Scalar types + */ +public abstract class TypeNil extends TypeScalar { + // 0 = high-subclass choice nil + // 1 = high-subclass no nil + // 2 = low -subclass no nil + // 3 = low -subclass also nil + byte _nil; + + TypeNil(byte t, byte nil ) { super(t); _nil = nil; } + + public static void gather(ArrayList ts) { } + + abstract TypeNil makeFrom(byte nil); + + byte xmeet0(TypeNil that) { return (byte)Math.max(_nil,that._nil); } + + byte dual0() { return (byte)(3-_nil); } + + // RHS is NIL + abstract Type meet0(); + + // RHS is XNIL + Type meetX() { + return _nil==0 ? XNIL : (_nil<=2 ? TypePtr.NPTR : TypePtr.PTR); + } + + Type nmeet(TypeNil tn) { + // Invariants: both are TypeNil subclasses and unequal classes. + // If this is TypePtr, we went to TypePtr.nmeet and not here. + // If that is TypePtr, this is not (invariant); reverse and go again. + if( tn instanceof TypePtr ts ) return ts.nmeet(this); + + // Two mismatched TypeNil, no Scalar. + if( _nil==0 && tn._nil==0 ) return XNIL; + if( _nil<=2 && tn._nil<=2 ) return TypePtr.NPTR; + return TypePtr.PTR; + } + + @Override public boolean isHigh () { return _nil <= 1; } + @Override public boolean isHighOrConst() { return isHigh() || isConstant(); } + + public boolean notNull() { return _nil==1 || _nil==2; } + public boolean nullable() { return _nil==3; } + + @Override boolean _isConstant() { return false; } + + final String q() { return _nil == 1 || _nil == 2 ? "" : "?"; } + final String x() { return isHigh() ? "~" : ""; } + + int hash() { return _nil<<17; } + + @Override boolean eq(Type ptr) { return ptr instanceof TypeNil nil && _nil ==nil._nil; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypePtr.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypePtr.java new file mode 100644 index 00000000..7e9e70e4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypePtr.java @@ -0,0 +1,57 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; + +/** + * Represents a Scalar; a single register-sized value. + */ +public class TypePtr extends TypeNil { + private static final String[] STRS = new String[]{"~ptr","~nptr","nptr","ptr"}; + + private TypePtr(byte nil) { super(TPTR,nil); } + + // An abstract pointer, pointing to either a Struct or an Array. + // Can also be null or not, so 4 choices {TOP,BOT} x {nil,not} + public static TypePtr XPTR = new TypePtr((byte)0).intern(); + public static TypePtr XNPTR= new TypePtr((byte)1).intern(); + public static TypePtr NPTR = (TypePtr)XNPTR.dual(); + public static TypePtr PTR = (TypePtr)XPTR.dual(); + private static final TypePtr[] PTRS = new TypePtr[]{XPTR,XNPTR,NPTR,PTR}; + + public static void gather(ArrayList ts) { ts.add(PTR); ts.add(NPTR); } + + TypeNil makeFrom(byte nil) { throw Utils.TODO("Should not reach here: plain pointer nil state is canonical"); } + + @Override public TypeNil xmeet(Type t) { + TypePtr that = (TypePtr) t; + return PTRS[xmeet0(that)]; + } + + @Override TypePtr xdual() { return new TypePtr((byte)(3-_nil)); } + + // High scalar loses, low scalar wins + @Override TypeNil nmeet(TypeNil tn) { + if( _nil==0 ) return tn; // High scalar loses + if( _nil==1 ) return tn.makeFrom(xmeet0(tn)); // High scalar loses + if( _nil==2 ) return tn._nil==3 ? PTR : NPTR; // Low scalar wins + return PTR; // this + } + + + // RHS is NIL + @Override Type meet0() { return isHigh() ? NIL : PTR; } + // RHS is XNIL + // 0->xscalar, 1->nscalar, 2->nscalar, 3->scalar + @Override Type meetX() { return _nil==0 ? XNIL : (_nil==3 ? PTR : NPTR); } + + @Override TypePtr _makeStorage() { return PTR; } + + + @Override public String str() { return STRS[_nil]; } + @Override public SB _print(SB sb, BitSet visit, boolean html) { return sb.p(str()); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeRPC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeRPC.java new file mode 100644 index 00000000..7acc99de --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeRPC.java @@ -0,0 +1,86 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import java.util.*; + +/** + * Return Program Control or Return PC or RPC + */ +public class TypeRPC extends Type { + + // A set of CallEndNode IDs (or StopNode); commonly just one. High-bit + // XInt sets represent complements/infinite sets. + public int[] _rpcs; + + private static final Ary FREE = new Ary<>(TypeRPC.class); + private TypeRPC init(int[] rpcs) { _rpcs = XInt.intern(rpcs); return this; } + private TypeRPC() { super(TRPC); } + private static TypeRPC malloc(int[] rpcs) { + return (FREE.isEmpty() ? new TypeRPC() : FREE.pop()).init(rpcs); + } + private static TypeRPC make(int[] rpcs) { + TypeRPC rpc = malloc(rpcs); + TypeRPC t2 = rpc.intern(); + return t2==rpc ? rpc : t2.free(rpc); + } + @Override TypeRPC free(Type t) { + TypeRPC rpc = (TypeRPC)t; + rpc._rpcs = null; + rpc._dual = null; + rpc._hash = 0; + FREE.push(rpc); + return this; + } + @Override boolean isFree() { return _rpcs==null; } + + public static TypeRPC constant(int cend) { + return make(XInt.make(cend)); + } + + public int rpc() { + return XInt.onlyBit(_rpcs); + } + + + public static final TypeRPC BOT = make(XInt.FULL); + private static final TypeRPC TEST2 = constant(2); + private static final TypeRPC TEST3 = constant(2); + public static final TypeRPC EMPTY = make(XInt.EMPTY); + + public static void gather(ArrayList ts) { ts.add(BOT); ts.add(TEST2); ts.add(TEST3); } + + @Override public TypeRPC makeZero() { return EMPTY; } + + + @Override public String str() { + if( _rpcs == XInt.FULL ) return "$[ALL]"; + if( _rpcs == XInt.EMPTY ) return "$[]"; + return "$"+XInt.str(_rpcs); + } + + @Override + public TypeRPC xmeet(Type other) { + TypeRPC rpc = (TypeRPC)other; + return make(XInt.meet(_rpcs,rpc._rpcs)); + } + + @Override TypeRPC xdual() { return malloc(XInt.dual(_rpcs)); } + @Override Type rdual() { + TypeRPC d = xdual(); + (_dual = d)._dual = this; + return d; + } + + @Override boolean _isConstant() { return XInt.isConstant(_rpcs); } + @Override TypeRPC _makeStorage() { return this; } + + + @Override + int hash() { return XInt.hash(_rpcs); } + @Override + public boolean eq( Type t ) { + TypeRPC rpc = (TypeRPC)t; // Contract + return _rpcs==rpc._rpcs; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeScalar.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeScalar.java new file mode 100644 index 00000000..e369e6a5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeScalar.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.SB; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; + +/** + * The envelope for values which can live in an ordinary register or memory + * field: integers, floats, memory pointers, and function pointers. + * + * `BOT` means any scalar value and `TOP` means no scalar value. They sit + * strictly inside the global {@link Type#BOTTOM}/{@link Type#TOP} pair, which + * also includes non-values such as control and memory. + */ +public class TypeScalar extends Type { + protected TypeScalar(byte type) { super(type); } + private TypeScalar() { this(TSCALAR); } + + public static final TypeScalar BOT = new TypeScalar().intern(); + public static final TypeScalar TOP = BOT.dual(); + + public static void gather(ArrayList ts) { ts.add(BOT); } + + /** Meet two different scalar families. */ + final Type smeet(TypeScalar scalar) { + if( _type == TSCALAR ) return isHigh() ? scalar : this; + if( scalar._type == TSCALAR ) return scalar.isHigh() ? this : scalar; + return BOT; + } + + @Override Type xmeet(Type t) { + TypeScalar scalar = (TypeScalar)t; + return isHigh() ? scalar : this; + } + + @Override TypeScalar xdual() { return new TypeScalar(); } + @Override public boolean isHigh() { return this == TOP; } + @Override Type _makeStorage() { return BOT; } + + + @Override public String str() { return isHigh() ? "~scalar" : "scalar"; } + @Override SB _print(SB sb, BitSet visit, boolean html) { return sb.p(str()); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeStruct.java new file mode 100644 index 00000000..67169009 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -0,0 +1,640 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.util.*; +import java.util.*; + +/** + * Represents a struct type. + */ +public class TypeStruct extends Type { + + // A Struct has a name and a set of fields; the fields themselves have + // names, types and aliases. The name is essentially another field, + // immutable and type ignored (BOT). + + /** + * `_open` and `_fref` are independent lattice coordinates. + * + * `_open` says the structural tail is not known yet. On the low side an + * open struct has infinitely many implicit BOT fields; its dual has an + * implicit TOP tail. A real declaration is therefore open while its body + * is being parsed: + * + * ```text + * struct S { int x; /* more declared fields may follow *\/ } + * ``` + * + * `_fref` says no actual declaration has contributed to the type. The + * parser may still invent fields demanded by uses, leaving a useful open + * structural constraint: + * + * ```text + * S? p; p.x; // fref S { BOT x; ... } + * ``` + * + * Meeting that constraint with a real definition ANDs `_fref` off while + * retaining and checking the invented field. Closing recursive types must + * not by itself clear `_fref`; closure discovers shape, not a declaration. + */ + + public String _name; // Struct name + public boolean _open; + public boolean _fref; + public Field[] _fields; + + private TypeStruct() { super(TSTRUCT); } + private static final Ary FREE = new Ary<>(TypeStruct.class); + private TypeStruct init(String name, boolean open, boolean fref, Field[] fields) { + // Check for const array element name with mutable field + //assert !name.startsWith("[~]") || fields.length!=2 || fields[1]._t==null || (fields[1]._final != fields[1]._t.isHigh()); + _name = name; + _open = open; + _fref = fref; + _fields = fields; + return this; + } + // Return a filled-in TypeStruct; either from free list or alloc new. + private static TypeStruct malloc(String name, boolean open, boolean fref, Field[] fields) { + return (FREE.isEmpty() ? new TypeStruct() : FREE.pop()).init(name,open,fref,fields); + } + // Free ts; return this. + @Override TypeStruct free(Type t) { + TypeStruct ts = (TypeStruct)t; + assert !ts.isFree() && !ts._terned; + ts._name = null; + ts._fields = null; + ts._offs = null; + ts._dual = null; + ts._hash = 0; + ts._aliases = null; + ts._fidxs = null; + FREE.push(ts); + return this; + } + @Override boolean isFree() { return _fields==null; } + + + // All fields directly listed + public static TypeStruct make(String name, boolean open, Field... fields) { + return make(name,open,false,fields); + } + public static TypeStruct make(String name, boolean open, boolean fref, Field... fields) { + TypeStruct ts = malloc(name, open, fref, fields); + TypeStruct t2 = ts.intern(); + if( t2==ts ) return ts; + return VISIT.isEmpty() ? t2.free(ts) : ts.delayFree(ts); + } + + // Summary aliases, after interning. Skips private + // fields which need to be explicitly loaded. + private int[] _aliases; + public int[] aliases() { + if( _aliases != null ) return _aliases; + if( _fields.length==0 ) return (_aliases=XInt.EMPTY); + assert _terned; + int maxAlias = 0; + for( Field f : _fields ) + maxAlias = Math.max(maxAlias,f._alias); + int len = XInt.idx(maxAlias)+1; + if( (maxAlias&31)==31 ) len++; + int[] xs = XInt.free(len); + for( Field f : _fields ) + if( f._fname.charAt(0) != '_' && // Skip private fields. + !hiddenInit(f) && // Skip private allocation helper. + f._t != Type.TOP ) // Skip uninit fields + xs[XInt.idx(f._alias)] |= XInt.mask(f._alias); + return (_aliases=XInt.intern(xs)); + } + + // Summary fidxs, after interning. Skips private + // fields which need to be explicitly loaded. + private int[] _fidxs; + public int[] fidxs() { + if( _fidxs != null ) return _fidxs; + assert _terned; + int[] xs = XInt.EMPTY; + for( Field f : _fields ) + if( f._fname.charAt(0) != '_' && // Skip private fields. + !hiddenInit(f) && // Skip private allocation helper. + f._t instanceof TypeFunPtr tfp ) // Looking for TFP + xs = XInt.meet(xs,tfp._fidxs); + return (_fidxs = xs); + } + + // With a user constructor, the same-named function field is the hidden + // default allocation helper, not part of the public class surface. + public boolean hiddenInit(Field fld) { + if( field("") == null || !_name.startsWith("class:") ) return false; + int idx = _name.lastIndexOf('.'); + String name = _name.substring(idx<0 ? "class:".length() : idx+1); + return fld._fname.equals(name); + } + + // New open struct with no fields + public static TypeStruct open( String name ) { return make(name,true); } + public static TypeStruct forward( String name ) { return make(name,true,true); } + public TypeStruct defined() { return _fref ? make(_name,_open,false,_fields) : this; } + + // Array, variant body + public static TypeStruct makeAry(String name, TypeInteger len, int lenAlias, Type body, int bodyAlias, boolean efinal) { + return make(name,false, + Field.make("#" , len, lenAlias, true ), + Field.make("[]",body,bodyAlias,efinal)); + } + // Array, known constant body + public static TypeStruct makeAry(String name, int lenAlias, int bodyAlias, TypeConAry con) { + Type len = TypeInteger.constant(con.len()); + return make(name,false, + Field.make("#" ,len , lenAlias,true), + Field.make("[]" ,con.elem(),bodyAlias,true), + Field.make("[con]",con ,bodyAlias,true)); + } + + public TypeStruct makeInit() { + Field[] fs = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) { + if( _fields[i]._extern ) { fs[i] = _fields[i]; continue; } + Type zero = _fields[i]._t.makeZero(); + fs[i] = _fields[i].makeFrom(_fields[i]._t != Type.BOTTOM && zero.isa(_fields[i]._t) ? zero : Type.TOP); + } + return make(_name,_open,_fref,fs); + } + + public TypeStruct add( Field f ) { + assert find(f._fname)==-1; // No double field names + Field[] flds = Arrays.copyOf(_fields,_fields.length+1); + flds[_fields.length] = f; + return make(_name,_open,_fref,flds); + } + public TypeStruct replace( Field f ) { + Field[] flds = Arrays.copyOf(_fields,_fields.length); + flds[find(f._fname)] = f; + return make(_name,_open,_fref,flds); + } + public TypeStruct remove( int i ) { + Field[] flds = new Field[_fields.length-1]; + System.arraycopy(_fields, 0,flds,0,i); + System.arraycopy(_fields,i+1,flds,i,flds.length-i); + return make(_name,_open,_fref,flds); + } + + public TypeStruct addOrUpdate(Field fld) { + int idx = find(fld._fname); + int len = idx == -1 ? (idx=_fields.length)+1 : _fields.length; + Field[] flds = Arrays.copyOf(_fields,len); + flds[idx] = fld; + return make(_name,_open,_fref,flds); + } + + public final TypeStruct close() { + return (TypeStruct)recurOpen()._close(_name, null).recurClose(); + } + @Override TypeStruct _close( String name, HashMap TYPES ) { + assert (name==null) != (TYPES==null); // One or the other + String namePlusFinal = isAry() ? (_name+(_fields[1]._final ? "~" : "!")).intern() : _name; + TypeStruct ts = (TypeStruct)VISIT.get(namePlusFinal); + if( ts!=null ) return ts; + TypeStruct base = name==null ? (TypeStruct)TYPES.get(_name) : this; + assert base != null; + boolean open = base._fref ? base._open : name != _name && name != null && _open; + ts = base.recurPre(namePlusFinal, _name, open, base._fref ); + Field[] flds = ts._fields; + // Now start the recursion + for( int i=0; i TYPES) { + TypeStruct base = (TypeStruct)TYPES.get(_name); + if( this == base ) return this; // Already the good form + TypeStruct ts = (TypeStruct)VISIT.get(_uid); + if( ts!=null ) return ts; + // Main work: replace open with closed + if( _open ) + return base; + // Build recursive version + ts = recurPre(_uid,_name,false,_fref); + Field[] flds = ts._fields; + // Now start the recursion + for( int i=0; i ts) { + ts.add(BOT); + ts.add(TEST); + ts.add(ARY); + ts.add(STR); + ts.add(ABC); + ts.add(FREF0); + ts.add(FREF1); + ts.add(FDEF1); + ts.add(SINT0); + ts.add(SFLT0); + ts.add(SINT1); + ts.add(SFLT1); + // Break cyclic init: built a struct + Field fcalloc = Field.make("calloc",TypeFunPtr.CALLOC,-2,true); + TypeStruct scalloc = make("calloc",false,fcalloc); + ts.add(scalloc); + + } + + // Find field index by name + public int find(String fname) { + for( int i=0; i<_fields.length; i++ ) + if( _fields[i]._fname==fname ) + return i; + return -1; + } + public Field field(String fname) { + int idx = find(fname); + return idx== -1 ? null : _fields[idx]; + } + + // Find field index by alias + public int findAlias( int alias ) { + for( int i=0; i<_fields.length; i++ ) + if( _fields[i]._alias==alias ) + return i; + return -1; + } + + @Override + TypeStruct xmeet(Type t) { + TypeStruct that = (TypeStruct) t; + assert !isFree() && !that.isFree(); + if( this==TOP ) return that; + if( that==TOP ) return this; + if( this==BOT ) return BOT; + if( that==BOT ) return BOT; + + // if equal, no matters. + // if short is BOT, chop ; recurPre on short. + // if short is TOP, copy long; recurPre on long. + TypeStruct min = _fields.length < that._fields.length ? this : that; + if( _fields.length != that._fields.length && (!min._open ^ min!=this) ) + return that.xmeet(this); + + assert (_name==that._name) == (_name.equals(that._name)); // Strings are interned + String name = + that._name==TOPNAME ? _name : + _name==TOPNAME ? that._name : + that._name==_name ? _name : + BOTNAME; + if( name==BOTNAME ) return BOT; + + // Check all other fields are sanely similar; same struct type but + // different field contents (e.g. field "age" is either 'int' or '18'). + int len = Math.min(_fields.length,that._fields.length); + for( int i=0; i= CodeGen.Phase.FinalizeIR.ordinal(); + // Compute a layout for a collection of fields + assert _fields != null; // No forward refs + + // Compute a layout + _offs = new int[_fields.length+1]; + if( isAry() ) { // Arrays used fixed u32 len, padding to elems, var length + assert _fields[1]._fname=="[]"; + // Pad out to the element alignment + int align = _fields[1]._t.alignment(); + _offs[1] = (4 + ((1<=0; i-- ) { + offs[i] = off; + off += cnts[i]<