├── .gitignore ├── LICENSE.md ├── README.md ├── build.sbt ├── core ├── build.sbt ├── project │ ├── build.properties │ └── effekt.sbt └── src │ └── main │ ├── java │ └── effekt │ │ ├── CPS.java │ │ ├── Continuation.java │ │ ├── Effectful.java │ │ ├── Effects.java │ │ ├── Effekt.java │ │ ├── Handler.java │ │ ├── Prog.java │ │ ├── Prompt.java │ │ ├── StatefulHandler.java │ │ ├── instrumentation │ │ ├── annotations │ │ │ ├── AlreadyInstrumented.java │ │ │ ├── DontInstrument.java │ │ │ └── Effectful.java │ │ └── exceptions │ │ │ └── NotInstrumented.java │ │ ├── runtime │ │ ├── Frame.java │ │ └── Runtime.java │ │ └── stateful │ │ ├── CPS.java │ │ ├── Continuation.java │ │ ├── State.java │ │ └── Stateful.java │ └── scala │ └── effekt │ └── runtime │ └── SplitSeq.scala ├── instrumentation └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ └── effekt │ ├── EffektClassLoader.scala │ └── instrumentation │ ├── Config.scala │ ├── Effekt.scala │ ├── EntryPoint.scala │ ├── Frame.scala │ ├── FreshNames.scala │ ├── InstrumentClass.scala │ ├── InstrumentEntryPoint.scala │ ├── InstrumentMethod.scala │ ├── InstrumentProject.scala │ ├── InstrumentationAnalysis.scala │ ├── JavaAgent.scala │ ├── MethodAnalysis.scala │ ├── MethodContext.scala │ └── package.scala ├── project ├── assembly.sbt └── build.properties └── sbtplugin └── src └── main └── scala ├── EffektPlugin.scala └── Keys.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Jonathan Brachthäuser 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java-Effekt 2 | An Implementation of effect handlers using bytecode manipulation. 3 | 4 | ## Examples 5 | - The world famous [Drunkflip](tests/src/test/java/run/amb/DrunkFlip.java) example. 6 | 7 | An example of some instrumented bytecode can be found [in this gist](https://gist.github.com/b-studios/28b9ed229369b962e0083989343d5ede). The source-file is [Simple.java](tests/src/test/java/run/Simple.java). 8 | 9 | ## Project Organization 10 | The project currently consists of the following subprojects: 11 | 12 | - [core](core) The runtime system of jvm-effekt -- contains necessary runtime classes (like Stack). 13 | - [instrumentation](instrumentation) The implementation of byte code instrumentation, depends on core. 14 | - [sbtplugin](sbtplugin) An sbtplugin for offline instrumentation of classfiles. 15 | - [tests](tests) A [separate](tests/build.sbt) project illustrating the use of Java Effekt. 16 | 17 | ## Dependencies 18 | The project currently depends on version `2.0.1` of the static analysis and bytecode generation framework [OPAL](https://bitbucket.org/delors/opal/overview). 19 | 20 | To run the tests, execute the following steps: 21 | ``` 22 | # If you haven't already, clone the repo 23 | git clone git@github.com:b-studios/java-effekt.git 24 | cd java-effekt 25 | 26 | # clean all previous builds, if any to avoid sbt conflicts (only necessary once in a while ;) ) 27 | rm -r **/target 28 | # Start sbt and publish the effekt instrumentation locally 29 | sbt publishLocal 30 | 31 | # now switch to the effekt core library and publish this locally 32 | cd core 33 | sbt publishLocal 34 | 35 | # switch to tests subproject and run tests 36 | cd .. 37 | git clone git@github.com:b-studios/java-effekt-tests.git 38 | cd java-effekt-tests 39 | sbt 40 | > test -- -oDF 41 | ``` 42 | 43 | Running the tests will instrument the class files and dump them for inspection under `target/instrumented`. 44 | To inspect bytecode, I highly recommend the 45 | [OPAL - Bytecode Disassembler Plugin](https://atom.io/packages/java-bytecode-disassembler) for Atom. 46 | 47 | ## Background 48 | [Scala-Effekt](https://github.com/b-studios/scala-effekt) is a library based implementation of 49 | effect handlers and uses a monad to implement multiprompt delimited continuations (which are 50 | necessary for effect handlers). 51 | Similarly, some experiments in Java show that this is in fact a viable approach. However, 52 | the monadic style might impose usability as well as performance issues. 53 | 54 | This project implements stack manipulation (a la [DelimCC](http://okmij.org/ftp/continuations/implementations.html)) on the JVM by 55 | only using the JVM stack for pure (non effectful) operations. For effectful operations, instead a 56 | user-level [`Stack`](core/src/main/java/effekt/Effekt.java) 57 | is used. Before each effectful operation, the continuation is pushed as a first-class frame to this stack. 58 | 59 | These first-class frames are instances of [`Frame`](core/src/main/java/effekt/runtime/Frame.java) 60 | and exactly represent the continuation after the effectful call. The method state is stored in the closure of the frame and restored before the original body is executed. 61 | 62 | This is very similar to the bytecode instrumentation performed by [Quasar](http://docs.paralleluniverse.co/quasar/). In particular, 63 | we also use Java checked exceptions to mark "effectful" functions. 64 | However, Quasar does not have a notion of first-class frames and still uses the JVM-stack for effectful functions. Thus resuming a 65 | continuation takes O(n) in the depth of the JVM stack. 66 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = project 2 | .in(file(".")) 3 | .settings(noPublishSettings) 4 | .aggregate(instrumentation, sbtplugin) 5 | 6 | lazy val effekt = "effekt" 7 | lazy val effektVersion = "0.1.2-SNAPSHOT" 8 | 9 | lazy val opalVersion = "2.0.1" 10 | 11 | lazy val effektSettings = Seq( 12 | scalaVersion := "2.12.4", 13 | organization := "de.b-studios", 14 | version := effektVersion 15 | ) 16 | 17 | // the instrumentation component and java agent 18 | lazy val instrumentation = project 19 | .in(file("instrumentation")) 20 | .settings(effektSettings) 21 | .settings(instrumentationSettings) 22 | .settings(javaAgentSettings) 23 | 24 | // the ahead of time sbt plugin 25 | lazy val sbtplugin = project 26 | .in(file("sbtplugin")) 27 | .settings(effektSettings) 28 | .settings(sbtpluginSettings) 29 | .dependsOn(instrumentation) 30 | 31 | 32 | lazy val instrumentationSettings = Seq( 33 | 34 | moduleName := s"${effekt}-instrumentation", 35 | name := s"${effekt}-instrumentation", 36 | 37 | libraryDependencies ++= Seq( 38 | "de.opal-project" % "common_2.12" % opalVersion, 39 | "de.opal-project" % "bytecode-representation_2.12" % opalVersion, 40 | "de.opal-project" % "bytecode-creator_2.12" % opalVersion, 41 | "de.opal-project" % "bytecode-assembler_2.12" % opalVersion, 42 | "de.opal-project" % "abstract-interpretation-framework_2.12" % opalVersion 43 | ) 44 | ) 45 | 46 | lazy val sbtpluginSettings = Seq( 47 | sbtPlugin := true, 48 | moduleName := s"${effekt}-sbtplugin", 49 | name := s"${effekt}-sbtplugin" 50 | ) 51 | 52 | lazy val noPublishSettings = Seq( 53 | publish := {}, 54 | publishLocal := {}, 55 | publishArtifact := false 56 | ) 57 | 58 | lazy val javaAgentSettings = Seq( 59 | test in assembly := {}, 60 | packageOptions in assembly += javaAgentManifest, 61 | packageOptions in ThisBuild += javaAgentManifest, 62 | packageOptions += javaAgentManifest 63 | ) 64 | 65 | lazy val javaAgentManifest = Package.ManifestAttributes( 66 | "Premain-Class" -> "effekt.instrumentation.JavaAgent", 67 | "Agent-Class" -> "effekt.instrumentation.JavaAgent", 68 | "Can-Retransform-Classes" -> "true", 69 | "Can-Redefine-Classes" -> "true" 70 | ) 71 | -------------------------------------------------------------------------------- /core/build.sbt: -------------------------------------------------------------------------------- 1 | import effekt.plugin.EffektPlugin 2 | 3 | lazy val effektName = "effekt" 4 | lazy val effektVersion = "0.1.2-SNAPSHOT" 5 | 6 | // the Effekt library (incl. Runtime) 7 | // we apply the effekt instrumentation also for the core library 8 | // since Handler and StatefulHandler both need to be instrumented. 9 | lazy val root = project 10 | .in(file(".")) 11 | .enablePlugins(EffektPlugin) 12 | .settings(EffektPlugin.effektDefaultSettings) 13 | .settings(EffektPlugin.instrumentAfterCompile) 14 | .settings( 15 | moduleName := s"${effektName}-core", 16 | name := s"${effektName}-core", 17 | organization := "de.b-studios", 18 | version := effektVersion, 19 | scalaVersion := "2.12.4" 20 | ) 21 | -------------------------------------------------------------------------------- /core/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.0.3 2 | -------------------------------------------------------------------------------- /core/project/effekt.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("de.b-studios" % "effekt-sbtplugin" % "0.1.2-SNAPSHOT") 2 | -------------------------------------------------------------------------------- /core/src/main/java/effekt/CPS.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | public interface CPS { 4 | B apply(Continuation k) throws Effects; 5 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/Continuation.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | public interface Continuation extends Prog { 4 | 5 | B resume(A value) throws Effects; 6 | 7 | default B apply() throws Effects { 8 | return resume(null); 9 | } 10 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/Effectful.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | /** 4 | * TODO add a family of interfaces: 5 | * Effectful () -> void 6 | * EffectfulIn A -> void 7 | * EffectfulOut () -> B 8 | * EffectfulInOut A -> B 9 | * 10 | * Same for CPS and StatefulCPS 11 | */ 12 | public interface Effectful { 13 | B apply(A a) throws Effects; 14 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/Effects.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | public class Effects extends Exception {} -------------------------------------------------------------------------------- /core/src/main/java/effekt/Effekt.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | import effekt.instrumentation.annotations.DontInstrument; 4 | import effekt.runtime.Runtime; 5 | 6 | @DontInstrument 7 | public final class Effekt extends Runtime { 8 | 9 | private Effekt() {} 10 | 11 | // --- Operations used by the instrumentation --- 12 | public static int resultI() throws Throwable { return (Integer) result(); } 13 | public static long resultJ() throws Throwable { return (Long) result(); } 14 | public static float resultF() throws Throwable { return (Float) result(); } 15 | public static double resultD() throws Throwable { return (Double) result(); } 16 | public static void resultVoid() throws Throwable { result(); } 17 | 18 | public static void returnWith(long result) { returnWith(Long.valueOf(result)); } 19 | public static void returnWith(int result) { returnWith(Integer.valueOf(result)); } 20 | public static void returnWith(float result) { returnWith(Float.valueOf(result)); } 21 | public static void returnWith(double result) { returnWith(Double.valueOf(result)); } 22 | public static void returnVoid() { returnWith(null); } 23 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/Handler.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | public interface Handler extends Prompt { 4 | 5 | Res pure(R r) throws Effects; 6 | 7 | default Res handle(Prog prog) throws Effects { 8 | return Effekt.pushPrompt(this, () -> pure(prog.apply())); 9 | } 10 | 11 | default A use(CPS body) throws Effects { 12 | return Effekt.withSubcontinuation(this, k -> 13 | body.apply(a -> 14 | Effekt.pushPrompt(this, () -> 15 | k.resume(a)))); 16 | } 17 | 18 | // only for compatibility: 19 | default A useOnce(CPS body) throws Effects { 20 | return use(body); 21 | } 22 | 23 | default A discard(Prog body) throws Effects { 24 | return use(k -> body.apply()); 25 | } 26 | 27 | static > E handle(H h, Effectful prog) throws Effects { 28 | return Effekt.pushPrompt(h, () -> h.pure(prog.apply(h))); 29 | } 30 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/Prog.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | public interface Prog { 4 | A apply() throws Effects; 5 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/Prompt.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | 4 | public interface Prompt {} -------------------------------------------------------------------------------- /core/src/main/java/effekt/StatefulHandler.java: -------------------------------------------------------------------------------- 1 | package effekt; 2 | 3 | import effekt.stateful.Stateful; 4 | 5 | public interface StatefulHandler extends Handler, Stateful { 6 | 7 | Res pure(R r) throws Effects; 8 | 9 | default Res handle(Prog prog, State init) throws Effects { 10 | importState(init); 11 | return Effekt.pushPrompt(this, () -> pure(prog.apply())); 12 | } 13 | 14 | // on stateful handlers 15 | // use(k -> ... k.resume(a) ...) should be equivalent to 16 | // useStateful( (k, s) -> ... k.resume(a, s) ... ) 17 | default A useStateful(effekt.stateful.CPS body) throws Effects { 18 | final State before = exportState(); 19 | return Effekt.withSubcontinuation(this, k -> 20 | body.apply((a, s) -> Effekt.pushPrompt(this, () -> { 21 | importState(s); 22 | return k.resume(a); 23 | }), 24 | before)); 25 | } 26 | 27 | default A use(CPS body) throws Effects { 28 | final State before = exportState(); 29 | return Effekt.withSubcontinuation(this, k -> 30 | body.apply(a -> 31 | Effekt.pushPrompt(this, () -> { 32 | importState(before); 33 | return k.resume(a); 34 | }))); 35 | } 36 | 37 | // only for compatibility: 38 | default A useOnce(effekt.stateful.CPS body) throws Effects { 39 | return useStateful(body); 40 | } 41 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/instrumentation/annotations/AlreadyInstrumented.java: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target({ElementType.METHOD, ElementType.TYPE}) 8 | public @interface AlreadyInstrumented {} 9 | -------------------------------------------------------------------------------- /core/src/main/java/effekt/instrumentation/annotations/DontInstrument.java: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target({ElementType.METHOD, ElementType.TYPE}) 8 | public @interface DontInstrument {} -------------------------------------------------------------------------------- /core/src/main/java/effekt/instrumentation/annotations/Effectful.java: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target({ElementType.METHOD, ElementType.TYPE}) 8 | public @interface Effectful {} -------------------------------------------------------------------------------- /core/src/main/java/effekt/instrumentation/exceptions/NotInstrumented.java: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation.exceptions; 2 | 3 | public class NotInstrumented extends RuntimeException { 4 | public static final NotInstrumented NOT_INSTRUMENTED = new NotInstrumented(); 5 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/runtime/Frame.java: -------------------------------------------------------------------------------- 1 | package effekt.runtime; 2 | 3 | import effekt.Effects; 4 | import effekt.instrumentation.annotations.DontInstrument; 5 | 6 | @DontInstrument 7 | public interface Frame { 8 | void enter() throws Effects, Throwable; 9 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/runtime/Runtime.java: -------------------------------------------------------------------------------- 1 | package effekt.runtime; 2 | 3 | import effekt.*; 4 | import effekt.instrumentation.annotations.DontInstrument; 5 | import scala.Tuple2; 6 | 7 | // CPS Runtime 8 | 9 | @DontInstrument 10 | public class Runtime { 11 | 12 | private static SplitSeq stack = EmptyCont$.MODULE$; 13 | private static Object result = null; 14 | 15 | private static Throwable lastException = null; 16 | 17 | private static Frame lastFrame = null; 18 | 19 | public static void push(Frame frame) { 20 | pushLastFrame(); 21 | lastFrame = frame; 22 | } 23 | public static void pop() { if (lastFrame != null) lastFrame = null; else stack = stack.tail(); } 24 | 25 | public static Object result() throws Throwable { 26 | if (lastException == null) 27 | return result; 28 | else 29 | throw lastException; 30 | } 31 | 32 | public static void returnWith(Object r) { 33 | result = r; lastException = null; 34 | } 35 | 36 | // clean up stack in case an effectful operation throws an exception 37 | public static void onThrow(Throwable t) throws Throwable { 38 | if (lastFrame != null) lastFrame = null; 39 | else stack = stack.tail(); 40 | throw t; 41 | } 42 | 43 | public static A run(Prog prog) { 44 | stack = EmptyCont$.MODULE$; 45 | lastFrame = prog::apply; 46 | trampoline(); 47 | 48 | if (lastException != null) { 49 | throw new RuntimeException("Exception during effectful execution", lastException); 50 | } 51 | 52 | return (A) result; 53 | } 54 | 55 | 56 | // Delimited Control Operators 57 | 58 | public static A pushPrompt(Prompt prompt, Prog prog) throws Effects { 59 | pushLastFrame(); 60 | stack = stack.pushPrompt(prompt); 61 | return prog.apply(); 62 | } 63 | 64 | private static void pushLastFrame() { 65 | if (lastFrame != null) { 66 | stack = stack.push(lastFrame); 67 | lastFrame = null; 68 | } 69 | } 70 | 71 | public static A withSubcontinuation(Prompt prompt, effekt.CPS body) throws Effects { 72 | pushLastFrame(); 73 | final Tuple2 res = stack.splitAt(prompt); 74 | final Segment init = res._1; 75 | final SplitSeq rest = res._2; 76 | 77 | lastFrame = null; 78 | stack = rest; 79 | // The following only works with `Config.pushInitialEntrypoint`, since otherwise the callsite will 80 | // not typecheck since it still expects a value of type A. 81 | // 82 | // return (A) body.apply(new Subcont<>(init)); 83 | body.apply(new Subcont<>(init)); 84 | return null; 85 | } 86 | 87 | private static void trampoline() { 88 | while (lastFrame != null || stack.nonEmpty()) { 89 | try { 90 | if (lastFrame != null) { 91 | Frame f = lastFrame; 92 | lastFrame = null; 93 | f.enter(); 94 | } else { 95 | final Tuple2 res = stack.pop(); 96 | stack = res._2; 97 | res._1.enter(); 98 | } 99 | } 100 | catch (Effects e) {} 101 | catch (Throwable t) { 102 | lastException = t; 103 | } 104 | } 105 | } 106 | 107 | @DontInstrument 108 | final static class Subcont implements Continuation { 109 | final Segment init; 110 | Subcont(Segment init) { this.init = init; } 111 | 112 | public R resume(A value) throws Effects { 113 | pushLastFrame(); 114 | stack = init.prependTo(stack); 115 | Effekt.returnWith(value); 116 | return null; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/stateful/CPS.java: -------------------------------------------------------------------------------- 1 | package effekt.stateful; 2 | 3 | import effekt.Effects; 4 | 5 | public interface CPS { 6 | B apply(Continuation k, S state) throws Effects; 7 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/stateful/Continuation.java: -------------------------------------------------------------------------------- 1 | package effekt.stateful; 2 | 3 | import effekt.Effects; 4 | 5 | public interface Continuation { 6 | B resume(A value, S state) throws Effects; 7 | } -------------------------------------------------------------------------------- /core/src/main/java/effekt/stateful/State.java: -------------------------------------------------------------------------------- 1 | package effekt.stateful; 2 | 3 | import scala.collection.immutable.Map; 4 | import scala.collection.immutable.Map$; 5 | 6 | public class State implements Stateful { 7 | 8 | private Map data = Map$.MODULE$.empty(); 9 | 10 | public Map exportState() { return data; } 11 | public void importState(Map state) { data = state; } 12 | 13 | public Field field(T init) { 14 | Field field = new Field(); 15 | data = data.updated(field, init); 16 | return field; 17 | } 18 | 19 | public class Field { 20 | public T get() { 21 | return (T) data.apply(this); 22 | } 23 | public void put(T value) { 24 | data = data.updated(this, value); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/java/effekt/stateful/Stateful.java: -------------------------------------------------------------------------------- 1 | package effekt.stateful; 2 | 3 | public interface Stateful { 4 | S exportState(); 5 | void importState(S state); 6 | } -------------------------------------------------------------------------------- /core/src/main/scala/effekt/runtime/SplitSeq.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package runtime 3 | 4 | import effekt.stateful.Stateful 5 | 6 | sealed trait SplitSeq { 7 | def isEmpty: Boolean 8 | def nonEmpty = !isEmpty 9 | def head: Frame 10 | def tail: SplitSeq 11 | def pop: (Frame, SplitSeq) 12 | def push(f: Frame): SplitSeq = f :: this // FramesCont(f, Nil, this) // surprisingly this is faster for the state benchmark 13 | def pushPrompt(p: Any) = PromptCont(p, this) 14 | def ::(f: Frame): SplitSeq = FramesCont(f, Nil, this) 15 | def :::(other: Segment): SplitSeq = other prependTo this 16 | 17 | def splitAt(p: Any): (Segment, SplitSeq) = splitAtAux(p, EmptySegment) 18 | 19 | // helper method with aggregator 20 | def splitAtAux(p: Any, seg: Segment): (Segment, SplitSeq) 21 | } 22 | case object EmptyCont extends SplitSeq { 23 | final val isEmpty = true 24 | final def head: Frame = sys error "No head on EmptyCont" 25 | final def tail: SplitSeq = sys error "No tail on EmptyCont" 26 | final def pop: (Frame, SplitSeq) = sys error "Can't pop EmptyCont" 27 | final def splitAtAux(p: Any, seg: Segment): (Segment, SplitSeq) = sys error s"Prompt not found $p in $seg" 28 | } 29 | 30 | // frame :: frames ::: rest 31 | case class FramesCont(final val frame: Frame, final val frames: List[Frame], final val rest: SplitSeq) extends SplitSeq { 32 | final val isEmpty = false 33 | final def head: Frame = frame 34 | final def tail: SplitSeq = pop._2 35 | final def pop: (Frame, SplitSeq) = { 36 | if (frames.isEmpty) 37 | (frame, rest) 38 | else 39 | (frame, FramesCont(frames.head, frames.tail, rest)) 40 | } 41 | override final def ::(f: Frame): SplitSeq = FramesCont(f, frame :: frames, rest) 42 | final def splitAtAux(p: Any, seg: Segment): (Segment, SplitSeq) = 43 | rest.splitAtAux(p, FramesSegment(frame, frames, seg)) 44 | } 45 | 46 | case class PromptCont( 47 | final val p: Any, 48 | final val rest: SplitSeq 49 | ) extends SplitSeq { 50 | 51 | final val isEmpty = rest.isEmpty 52 | final def head: Frame = rest.head 53 | final def tail: SplitSeq = rest.tail 54 | final def pop: (Frame, SplitSeq) = rest.pop 55 | 56 | final def splitAtAux(p2: Any, seg: Segment): (Segment, SplitSeq) = { 57 | 58 | // we always save the current state at the point of capture in the segment 59 | if (p2.asInstanceOf[AnyRef] eq p.asInstanceOf[AnyRef]) { 60 | (seg, rest) 61 | } else { 62 | val currState = p match { 63 | case s: Stateful[x] => s.exportState() 64 | case _ => null 65 | } 66 | rest.splitAtAux(p2, PromptSegment(p, currState, seg)) 67 | } 68 | } 69 | } 70 | 71 | // sub continuations / stack segments 72 | // mirrors the stack, and so is in reverse order. allows easy access to the state 73 | // stored in the current prompt 74 | sealed trait Segment { 75 | def prependTo(stack: SplitSeq): SplitSeq 76 | } 77 | case object EmptySegment extends Segment { 78 | def prependTo(stack: SplitSeq): SplitSeq = stack 79 | } 80 | case class FramesSegment(final val frame: Frame, final val frames: List[Frame], final val init: Segment) extends Segment { 81 | def prependTo(stack: SplitSeq): SplitSeq = init prependTo FramesCont(frame, frames, stack) 82 | } 83 | case class PromptSegment( 84 | final val prompt: Any, 85 | final val state: Any, 86 | final val init: Segment) extends Segment { 87 | def prependTo(stack: SplitSeq): SplitSeq = { 88 | // restore state as of the time of capture 89 | prompt match { 90 | case s: Stateful[Any] => s.importState(state) 91 | case _ => 92 | } 93 | init prependTo PromptCont(prompt, stack) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /instrumentation/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | org.opalj.ba { 2 | CODE { 3 | logDeadCode = true 4 | } 5 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/EffektClassLoader.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | 3 | import scala.collection.mutable 4 | 5 | /** 6 | * A simple `ClassLoader` that looks-up the available classes in a standard map. 7 | * Based on the OPAL class loader. 8 | * https://bitbucket.org/delors/opal/src/HEAD/OPAL/common/src/main/scala/org/opalj/util/InMemoryClassLoader.scala?at=develop&fileviewer=file-view-default 9 | * 10 | * 11 | * ORIGINAL LICENSE (OPAL Project) 12 | * ------------------------------- 13 | * BSD 2-Clause License: 14 | * Copyright (c) 2009 - 2018 15 | * Software Technology Group 16 | * Department of Computer Science 17 | * Technische Universität Darmstadt 18 | * All rights reserved. 19 | * 20 | * Redistribution and use in source and binary forms, with or without 21 | * modification, are permitted provided that the following conditions are met: 22 | * 23 | * - Redistributions of source code must retain the above copyright notice, 24 | * this list of conditions and the following disclaimer. 25 | * - Redistributions in binary form must reproduce the above copyright notice, 26 | * this list of conditions and the following disclaimer in the documentation 27 | * and/or other materials provided with the distribution. 28 | * 29 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 30 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 31 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 32 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 33 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 34 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 35 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 36 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 37 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 38 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 39 | * POSSIBILITY OF SUCH DAMAGE. 40 | */ 41 | class EffektClassLoader( 42 | val db: Map[String, Array[Byte]], 43 | parent: ClassLoader 44 | ) extends ClassLoader(parent) { 45 | 46 | private val classCache = mutable.HashMap.empty[String, Class[_]] 47 | 48 | private def loadOrDefineInstrumentedClass(name: String): Class[_] = { 49 | classCache.getOrElseUpdate(name, db.get(name) match { 50 | case Some(data) ⇒ defineClass(name, data, 0, data.length) 51 | case None ⇒ throw new ClassNotFoundException(name) 52 | }) 53 | } 54 | 55 | @throws[ClassNotFoundException] 56 | override def findClass(name: String): Class[_] = { 57 | if (!db.contains(name)) return super.findClass(name) 58 | 59 | loadOrDefineInstrumentedClass(name) 60 | } 61 | 62 | @throws[ClassNotFoundException] 63 | override def loadClass(name: String, resolve: Boolean): Class[_] = { 64 | if (!db.contains(name)) return super.loadClass(name, resolve) 65 | 66 | loadOrDefineInstrumentedClass(name) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/Config.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package instrumentation 3 | 4 | case class Config( 5 | bubbleSemantics: Boolean = false, // requires BubbleRuntime 6 | pushInitialEntrypoint: Boolean = false, // requires CPSRuntime 7 | optimizePureCalls: Boolean = false // requires CPSLocalRuntime or bubbleSemantics 8 | ) -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/Effekt.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package instrumentation 3 | 4 | import org.opalj.br._ 5 | import org.opalj.br.instructions._ 6 | import MethodDescriptor._ 7 | 8 | object Effekt { 9 | 10 | val Type = ObjectType("effekt/Effekt") 11 | 12 | // assumes the frame is on top of the operand stack 13 | def push = code ( 14 | INVOKESTATIC(Type, false, "push", MethodDescriptor(RefArray(Frame.Type), VoidType)) 15 | ) 16 | 17 | def onThrow = code ( 18 | INVOKESTATIC(Type, false, "onThrow", MethodDescriptor(RefArray(ObjectType.Throwable), VoidType)) 19 | ) 20 | 21 | def pop = code ( 22 | INVOKESTATIC(Type, false, "pop", NoArgsAndReturnVoid) 23 | ) 24 | 25 | def beforeEffect = code ( 26 | INVOKESTATIC(Type, false, "beforeEffect", NoArgsAndReturnVoid) 27 | ) 28 | 29 | def isEffectful = code ( 30 | INVOKESTATIC(Type, false, "isEffectful", JustReturnsBoolean) 31 | ) 32 | 33 | def isPure = code ( 34 | INVOKESTATIC(Type, false, "isPure", JustReturnsBoolean) 35 | ) 36 | 37 | // the result of the method is currently top on the stack 38 | def returnWith(retType: Type) = 39 | if (retType.isVoidType) code( 40 | INVOKESTATIC(Type, false, "returnVoid", NoArgsAndReturnVoid) 41 | ) else code ( 42 | if (retType.computationalType.operandSize == 2) DUP2 else DUP, 43 | INVOKESTATIC(Type, false, "returnWith", MethodDescriptor(toComputational(retType), VoidType)) 44 | ) 45 | 46 | def result(typ: Type) = 47 | if (typ.isVoidType) 48 | code (INVOKESTATIC(Type, false, "resultVoid", NoArgsAndReturnVoid)) 49 | else 50 | apply("result", typ.asFieldType)() 51 | 52 | // use like: 53 | // stack("get", IntegerType)() for GlobalStack.get(): Int 54 | def apply(name: String, retType: FieldType)(argTypes: FieldType*) = (retType, toComputational(retType)) match { 55 | case (t : ReferenceType, ct) => 56 | code (INVOKESTATIC(Type, false, name, MethodDescriptor(argTypes.toRefArray, ObjectType.Object)), CHECKCAST(t)) 57 | case (t, ct) => 58 | code (INVOKESTATIC(Type, false, name + ct.toJVMTypeName, MethodDescriptor(argTypes.toRefArray, ct))) 59 | } 60 | 61 | private def toComputational(t: Type): FieldType = 62 | t.computationalType match { 63 | case ComputationalTypeDouble => DoubleType 64 | case ComputationalTypeFloat => FloatType 65 | case ComputationalTypeInt => IntegerType 66 | case ComputationalTypeLong => LongType 67 | case ComputationalTypeReference => ObjectType.Object 68 | // TODO support ReturnAddress 69 | case _ => ??? 70 | } 71 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/EntryPoint.scala: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation 2 | 3 | import org.opalj.br.PC 4 | 5 | // the label of the entrypoint AFTER the effectful call at position 6 | // `callPos`. 7 | case class EntryPoint(callPos: PC, index: Int) { 8 | def label = Symbol(s"EP$index") 9 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/Frame.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package instrumentation 3 | 4 | import org.opalj.br._ 5 | import org.opalj.br.instructions._ 6 | import MethodDescriptor._ 7 | 8 | object Frame { 9 | 10 | val QualifiedName = "effekt/runtime/Frame" 11 | val Type = ObjectType(QualifiedName) 12 | 13 | def createClosure(implClass: ObjectType, implName: String, closureArgs: RefArray[FieldType]) = { 14 | 15 | // for now we ALWAYS generate static methods and use static method handles 16 | // since `this` needs to be passed as argument anyways. 17 | val methodHandle = 18 | InvokeStaticMethodHandle( 19 | implClass, 20 | 21 | // TODO figure out how to determine this info 22 | false, // isInterface: Boolean, 23 | implName, 24 | // closure arguments -> standard arguments -> return type 25 | MethodDescriptor(closureArgs, VoidType) 26 | ) 27 | 28 | val methodType = NoArgsAndReturnVoid 29 | 30 | val bootstrap = BootstrapMethod( 31 | metafactoryHandle, 32 | RefArray(methodType, methodHandle, methodType)) 33 | 34 | val closureType = MethodDescriptor(closureArgs, Type) 35 | 36 | code( 37 | DEFAULT_INVOKEDYNAMIC(bootstrap, "enter", closureType) 38 | ) 39 | } 40 | 41 | // Method Handle for a metafactory call 42 | lazy val metafactoryHandle = { 43 | import ObjectType._ 44 | InvokeStaticMethodHandle( 45 | LambdaMetafactory, 46 | false, 47 | "metafactory", 48 | MethodDescriptor(RefArray(MethodHandles$Lookup, String, MethodType, MethodType, MethodHandle, MethodType), CallSite) 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/FreshNames.scala: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation 2 | 3 | import scala.collection.mutable 4 | 5 | case class FreshNames() { 6 | val usage = mutable.HashMap.empty[String, Int] 7 | 8 | def apply(name: String): String = { 9 | val used = usage.getOrElse(name, 0) 10 | usage.put(name, used + 1) 11 | s"${name}${used + 1}" 12 | } 13 | } 14 | 15 | case class FreshIds() { 16 | var last = 0; 17 | 18 | def apply(): Int = { 19 | val res = last 20 | last += 1 21 | res 22 | } 23 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/InstrumentClass.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package instrumentation 3 | 4 | import annotationTypes.{ AlreadyInstrumented, DontInstrument } 5 | 6 | import org.opalj.br._ 7 | import org.opalj.br.analyses.Project 8 | import org.opalj.ba._ 9 | import org.opalj.collection.immutable.UShortPair 10 | 11 | import java.net.URL 12 | 13 | object InstrumentedClass { 14 | 15 | // TODO factor this information to a better place 16 | val classfileVersion = UShortPair(0, 52) 17 | 18 | def apply(cf: ClassFile, p: Project[URL], cfg: Config): Option[ClassFile] = { 19 | 20 | if (shouldBeIgnored(cf)) 21 | return None 22 | 23 | // symgen for new fun classes 24 | val fresh = FreshNames() 25 | 26 | def toTemplate[T](m: METHOD[T]): MethodTemplate = 27 | m.result(classfileVersion, cf.thisType)._1 28 | 29 | var instrumented = false 30 | 31 | val (ms, eps) = cf.methods.map { m => 32 | val ctx = MethodContext(m, p, fresh, cfg) 33 | val instr = InstrumentMethod(ctx) 34 | if (!instr.needsInstrumentation) { 35 | (List(m.copy()), Nil) 36 | } else { 37 | instrumented = true 38 | val InstrumentMethod.Result(stub, entrypoints) = instr.instrument 39 | (List(stub), entrypoints.map(toTemplate)) 40 | } 41 | }.unzip match { 42 | case (ms, eps) => (ms.flatten, eps.flatten) 43 | } 44 | 45 | if (!instrumented) 46 | return None 47 | 48 | Some(cf.copy( 49 | version = classfileVersion, 50 | methods = (ms ++ eps).toRefArray, 51 | interfaceTypes = cf.interfaceTypes ++ Seq(Frame.Type), 52 | attributes = cf.attributes :+ alreadyInstrumented 53 | )) 54 | } 55 | 56 | def shouldBeIgnored(cf: ClassFile): Boolean = { 57 | val annos = cf.annotations.map { _.annotationType } 58 | annos.contains(AlreadyInstrumented.Type) || annos.contains(DontInstrument.Type) 59 | } 60 | 61 | lazy val alreadyInstrumented: AnnotationTable = 62 | RuntimeVisibleAnnotationTable(RefArray(Annotation(AlreadyInstrumented.Type))) 63 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/InstrumentEntryPoint.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package instrumentation 3 | 4 | import org.opalj.br._ 5 | import org.opalj.br.instructions._ 6 | import org.opalj.ba._ 7 | import org.opalj.collection.mutable.Locals 8 | 9 | /** 10 | * Manages state around entry points 11 | */ 12 | case class InstrumentEntryPoint( 13 | ctx: MethodContext, 14 | analyser: MethodAnalysis) { 15 | 16 | import ctx._ 17 | 18 | // First store all operands, then all locals 19 | trait State { 20 | 21 | // name of the entry point 22 | lazy val name: String = fresh(s"${m.name}$$entrypoint$$") 23 | 24 | // the operand stack (in reverse order, bottom first) 25 | def operands: List[FieldType] 26 | def locals: Locals[FieldType] 27 | def alive: List[Int] 28 | 29 | // types of all operands and locals that need to be stored 30 | def types: RefArray[FieldType] = (operands ++ alive.map(locals.apply)).toRefArray 31 | 32 | def debug = println(s"Operands: $operands\nLocals: $locals\nAlive: $alive\nTypes: $types") 33 | 34 | // first index in instrumented method that is used by an closure argument 35 | lazy val firstArgIndex = 0 // if (m.isStatic) 0 else 1 36 | 37 | // now saving locals is just pushing them on the operand stack and 38 | // invoking closure creation 39 | // the operands are already on the operand stack 40 | def saveState = alive.flatMap { local => 41 | code (LoadLocalVariableInstruction(locals(local), local)) 42 | } 43 | 44 | def restoreState = restoreOperands ++ loadLocals ++ restoreLocals 45 | 46 | // compute indices that will be used to store the current state 47 | lazy val (operandIndices, firstLocal) = indices(operands.toList, firstArgIndex) 48 | lazy val (localIndices, _) = indices(alive.map(locals.apply), firstLocal) 49 | 50 | def restoreOperands = (operands zip operandIndices).flatMap { case (operand, index) => 51 | code (LoadLocalVariableInstruction(operand, index)) 52 | } 53 | 54 | def loadLocals = (alive zip localIndices).flatMap { case (local, index) => 55 | code (LoadLocalVariableInstruction(locals(local), index)) 56 | } 57 | 58 | def restoreLocals = alive.reverse.flatMap { local => 59 | code (StoreLocalVariableInstruction(locals(local), local)) 60 | } 61 | 62 | // helper function that computes indices based on the size (1 or 2 registers) of 63 | // the given fieldtypes. 64 | def indices(ts: List[FieldType], startFrom: Int = 0): (List[Int], Int) = { 65 | val (is, next) = ts.foldLeft[(List[Int], Int)]((Nil, startFrom)) { 66 | case ((rest, next), el) => (next :: rest, next + el.operandSize) 67 | } 68 | (is.reverse, next) 69 | } 70 | 71 | // push 72 | def pushFun = Frame.createClosure(m.classFile.thisType, name, types) ++ Effekt.push 73 | } 74 | 75 | // Reads the locals from the method descriptor. 76 | // Operand stack is empty 77 | object EntryState extends State { 78 | 79 | val parameters = m.descriptor.parameterTypes 80 | def operands: List[FieldType] = Nil 81 | 82 | val locals: Locals[FieldType] = 83 | if (m.isStatic) 84 | Locals(parameters.toIndexedSeq) 85 | else 86 | Locals((m.classFile.thisType +: parameters).toIndexedSeq) 87 | 88 | def alive: List[Int] = 89 | if (m.isStatic) 90 | (0 until m.descriptor.parametersCount).toList 91 | else // now we also need to store `this` in the closure again 92 | (0 to m.descriptor.parametersCount).toList 93 | } 94 | 95 | trait LocalState extends State { 96 | 97 | // information needed for all entrypoint related utilities 98 | val ep: EntryPoint 99 | val firstTempLocal: Int 100 | 101 | // provided methods: 102 | def beforeCall: Seq[CodeElement[AnyRef]] 103 | def afterCall(returnValue: Boolean): Seq[CodeElement[AnyRef]] 104 | 105 | def beforeTailCall: Seq[CodeElement[AnyRef]] 106 | def afterTailCall(returnValue: Boolean): Seq[CodeElement[AnyRef]] 107 | 108 | 109 | // === Called Effectful Function Information === 110 | val callPos = ep.callPos 111 | lazy val effectOp = body.instructions(callPos).asInvocationInstruction 112 | lazy val effectRet = effectOp.methodDescriptor.returnType 113 | lazy val isVoid = effectRet.isVoidType 114 | lazy val nextPos = body.pcOfNextInstruction(callPos) 115 | 116 | 117 | // === Locals and Operands Information === 118 | 119 | // Since we get operand stack information AFTER the call, we need 120 | // to ignore the result on top of the stack, which is NOT on top 121 | // of the stack at point of storing. 122 | // 123 | // we also reverse the operands since they will be stored in the closure 124 | // bottom to top. 125 | lazy val operandsWithResult = analyser.operandsAt(nextPos) 126 | lazy val operands = (if (isVoid) operandsWithResult else operandsWithResult.tail).reverse.toList 127 | lazy val locals = analyser.localsAt(nextPos) 128 | lazy val alive = analyser.aliveAt(nextPos) 129 | lazy val callOperands: List[FieldType] = { 130 | val params = effectOp.methodDescriptor.parameterTypes.toList 131 | if (effectOp.isInstanceMethod) { 132 | List(ObjectType.Object) ++ params 133 | } else { 134 | params 135 | } 136 | } 137 | 138 | // === Operand indexes into temporary registers === 139 | type OperandMap = List[(FieldType, Int)] 140 | lazy val (callOpIndices, firstOperandIndex) = indices(callOperands, firstTempLocal) 141 | lazy val (restOpIndices, tmpResultIndex) = indices(operands, firstOperandIndex) 142 | 143 | lazy val callOperandsMap: OperandMap = callOperands zip callOpIndices 144 | lazy val restOperandsMap: OperandMap = operands zip restOpIndices 145 | lazy val resultMap: OperandMap = if (isVoid) Nil else List((effectRet.asFieldType, tmpResultIndex)) 146 | 147 | 148 | // === Result Handling === 149 | lazy val ignoreResult = 150 | if (effectRet.isVoidType) code () 151 | else if (effectRet.computationalType.operandSize == 2) code (POP2) 152 | else code (POP) 153 | 154 | 155 | // === Exception Handling === 156 | 157 | // this is only needed since we don't explicitly push the initial entrypoint 158 | def beginHandling = code(TRY(Symbol(s"EH${name}"))) 159 | def endHandling(returnValue: Boolean) = 160 | code(TRYEND(Symbol(s"EH${name}")), CATCH(Symbol(s"EH${name}"), -1, Some(ObjectType.Throwable))) ++ 161 | Effekt.onThrow ++ maybeReturnDefault(returnValue) 162 | 163 | 164 | // === Save and Restore Call Operands === 165 | def save(m: OperandMap) = m.reverse.flatMap { 166 | case (op, idx) => saveAt(op, idx) 167 | } 168 | def restore(m: OperandMap) = m.flatMap { 169 | case (op, idx) => restoreFrom(op, idx) 170 | } 171 | 172 | def saveCallOperands = save(callOperandsMap) 173 | def restoreCallOperands = restore(callOperandsMap) 174 | 175 | def saveRestOperands = save(restOperandsMap) 176 | def restoreRestOperands = restore(restOperandsMap) 177 | 178 | def saveResult = save(resultMap) 179 | def restoreResult = restore(resultMap) 180 | 181 | // index relative to stack index 182 | def saveAt(t: FieldType, index: Int) = 183 | code(StoreLocalVariableInstruction(t, index)) 184 | 185 | def restoreFrom(t: FieldType, index: Int) = 186 | code(LoadLocalVariableInstruction(t, index)) 187 | 188 | 189 | def maybeReturn(returnValue: Boolean) = 190 | if (returnValue) code (ReturnInstruction(m.returnType)) else code (RETURN) 191 | 192 | def maybeReturnDefault(returnValue: Boolean) = 193 | if (returnValue) returnDefaultValue(m.returnType) else code (RETURN) 194 | 195 | } 196 | 197 | case class CPSState(ep: EntryPoint, firstTempLocal: Int) extends LocalState { 198 | 199 | def beforeCall = 200 | saveCallOperands ++ 201 | saveState ++ 202 | pushFun ++ 203 | restoreCallOperands ++ 204 | beginHandling 205 | 206 | // now that we don't use exceptions anymore (but default values) the 207 | // previous call NEEDS to be effectful. We just return. 208 | def afterCall(returnValue: Boolean) = 209 | maybeReturnDefault(returnValue) ++ 210 | endHandling(returnValue) ++ 211 | code (ep.label) ++ 212 | restoreState ++ 213 | Effekt.result(effectRet) 214 | 215 | def beforeTailCall = code() 216 | def afterTailCall(returnValue: Boolean) = code() 217 | } 218 | 219 | // resumes in place 220 | case class CPSInPlaceState(ep: EntryPoint, firstTempLocal: Int) extends LocalState { 221 | 222 | def beforeCall = 223 | // (1) save the full operand stack 224 | saveCallOperands ++ 225 | saveRestOperands ++ 226 | // (2) only restore the part that should be closed over and save it in the closure 227 | restoreRestOperands ++ 228 | saveState ++ 229 | pushFun ++ 230 | // (3) restore the full operand stack to allow in-place resumption 231 | restoreRestOperands ++ 232 | restoreCallOperands ++ 233 | beginHandling 234 | 235 | def afterCall(returnValue: Boolean) = 236 | Effekt.isEffectful ++ 237 | code (IFEQ(popLabel)) ++ 238 | // (4) was effectful, so return to trampoline 239 | maybeReturnDefault(returnValue) ++ 240 | endHandling(returnValue) ++ 241 | code (popLabel) ++ 242 | // (5) remove stack frame that has been pushed, but is not needed 243 | Effekt.pop ++ 244 | code (GOTO(resumeLabel)) ++ 245 | code (ep.label) ++ 246 | restoreState ++ 247 | Effekt.result(effectRet) ++ 248 | code (resumeLabel) 249 | 250 | def beforeTailCall = Effekt.beforeEffect 251 | def afterTailCall(returnValue: Boolean) = 252 | if (returnValue) code() else 253 | Effekt.isPure ++ 254 | code (IFEQ(exit)) ++ 255 | Effekt.returnWith(effectRet) ++ 256 | code (exit) ++ 257 | code (RETURN) 258 | 259 | lazy val popLabel = Symbol(ep.label.name ++ "Pop") 260 | lazy val resumeLabel = Symbol(ep.label.name ++ "Resume") 261 | lazy val exit = Symbol(fresh.apply("exit")) 262 | } 263 | 264 | case class BubbleSemanticsState(ep: EntryPoint, firstTempLocal: Int) extends LocalState { 265 | 266 | def beforeCall = Effekt.beforeEffect 267 | 268 | // now that we don't use exceptions anymore (but default values) the 269 | // previous call NEEDS to be effectful. We just return. 270 | def afterCall(returnValue: Boolean) = 271 | Effekt.isEffectful ++ 272 | code (IFEQ(resumeLabel)) ++ 273 | ignoreResult ++ 274 | saveState ++ 275 | pushFun ++ 276 | maybeReturnDefault(returnValue) ++ 277 | code (ep.label) ++ 278 | restoreState ++ 279 | Effekt.result(effectRet) ++ 280 | code (resumeLabel) 281 | 282 | def beforeTailCall = Effekt.beforeEffect 283 | 284 | def afterTailCall(returnValue: Boolean) = 285 | if (returnValue) code() else 286 | Effekt.isPure ++ 287 | code (IFEQ(exit)) ++ 288 | Effekt.returnWith(effectRet) ++ 289 | code (exit) ++ 290 | code (RETURN) 291 | 292 | lazy val resumeLabel = Symbol(ep.label.name ++ "Resume") 293 | lazy val exit = Symbol(fresh.apply("exit")) 294 | } 295 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/InstrumentMethod.scala: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation 2 | 3 | import java.net.URL 4 | 5 | import effekt._ 6 | import org.opalj.ba._ 7 | import org.opalj.br._ 8 | import org.opalj.br.analyses.Project 9 | import org.opalj.br.instructions._ 10 | 11 | case class InstrumentMethod(ctx: MethodContext) extends InstrumentationAnalysis { 12 | 13 | import ctx._ 14 | 15 | // instantiate other components 16 | lazy val analyser = MethodAnalysis(ctx) 17 | lazy val eps = InstrumentEntryPoint(ctx, analyser) 18 | import eps._ 19 | 20 | lazy val className = m.classFile.thisType.fqn 21 | 22 | lazy val instrument = { 23 | if (config.pushInitialEntrypoint) { 24 | InstrumentMethod.Result( 25 | pushingInitialEntryMethodStub, 26 | generateInitialEntrypointMethod +: generateEntrypointMethods) 27 | } else { 28 | InstrumentMethod.Result( 29 | initialEntryWithModifiedReturns, 30 | generateEntrypointMethods) 31 | } 32 | } 33 | 34 | def instrumentState: (EntryPoint, Int) => LocalState = 35 | if (config.bubbleSemantics) BubbleSemanticsState 36 | else if (config.optimizePureCalls) CPSInPlaceState 37 | else CPSState 38 | 39 | lazy val entrypointsState = entrypoints.map { ep => (ep, instrumentState(ep, firstTempLocal)) } 40 | 41 | // does not push a frame, but IS the initial entrypoint while maintaining the 42 | // original signature 43 | lazy val initialEntryWithModifiedReturns: MethodTemplate = { 44 | val lcode = generateInstrumentedBody(true) 45 | val (stub, _) = lcode.result(InstrumentedClass.classfileVersion, m) 46 | m.copy(body = Some(stub)) 47 | } 48 | 49 | // just a stub pushing the initial entrypoint and immediately returning 50 | lazy val pushingInitialEntryMethodStub = { 51 | 52 | // load arguments, save in closure, push fun, throw 53 | val stubCode = EntryState.loadLocals ++ EntryState.pushFun ++ returnDefaultValue(m.descriptor.returnType) 54 | 55 | // should also be ok to use the original classfile version here, right? 56 | val (stub, _) = CODE(stubCode: _*)(InstrumentedClass.classfileVersion, m) 57 | m.copy(body = Some(stub)) 58 | } 59 | 60 | lazy val generateInitialEntrypointMethod = { 61 | val lcode = generateInstrumentedBody(false) 62 | val flags = PRIVATE.STATIC.SYNTHETIC 63 | val descriptor = MethodDescriptor(EntryState.types, VoidType) 64 | 65 | // at the beginning of the initial entrypoint we don't need to do anything with 66 | // our state. 67 | // 68 | // there were no operands and the locals are already in place. 69 | METHOD(flags, EntryState.name, descriptor.toJVMDescriptor, lcode.result) 70 | } 71 | 72 | lazy val generateEntrypointMethods = for ((ep, state) <- entrypointsState) yield { 73 | val lcode = generateInstrumentedBody(false) 74 | val flags = PRIVATE.STATIC.SYNTHETIC 75 | val descriptor = MethodDescriptor(state.types, VoidType) 76 | 77 | // At the beginning of each individual entrypoint method add: 78 | // (1) restoration code 79 | // (2) unconditional jump to entrypoint 80 | // 81 | // OPAL's dead code elimination will reduce the method body to a minimum 82 | lcode.insert(0, InsertionPosition.At, code(GOTO(ep.label))) 83 | 84 | METHOD(flags, state.name, descriptor.toJVMDescriptor, lcode.result) 85 | } 86 | 87 | def generateInstrumentedBody(returnValue: Boolean): LabeledCode = { 88 | assert(m.body.isDefined, "Can only instrument concrete methods.") 89 | 90 | val lcode = LabeledCode(body) 91 | 92 | // add code before returns 93 | // add cast before reference return, just to make sure bytecode verification works 94 | def ret = (returnValue, m.returnType) match { 95 | case (true, t: ReferenceType) => code (CHECKCAST(t), ReturnInstruction(t)) 96 | case (true, t) => code (ReturnInstruction(t)) 97 | case (false, _) => code (RETURN) 98 | } 99 | 100 | // for every return instruction in the *original* bytecode: 101 | for (PCAndInstruction(pc, ReturnInstruction(r)) <- body) { 102 | // for effectful tail calls: 103 | // - do not call returnWith (since the result is a dummy value) 104 | // - just replace with return 105 | // 106 | // this causes a problem if the effecful call turns out to be pure 107 | // and we optimize for pure calls. In that case the result wont be 108 | // stored... 109 | if (tailEffectCallReturns contains pc) { 110 | lcode.replace(pc, ret) 111 | } else { 112 | lcode.replace(pc, Effekt.returnWith(m.returnType) ++ ret) 113 | } 114 | } 115 | 116 | // mark entry points 117 | for ((ep, state) <- entrypointsState) { 118 | lcode.insert(ep.callPos, InsertionPosition.Before, state.beforeCall) 119 | lcode.insert(ep.callPos, InsertionPosition.After, state.afterCall(returnValue)) 120 | } 121 | 122 | // deal with tail calls 123 | for (pc <- tailcalls) { 124 | val tailCallEP = EntryPoint(pc, -1) 125 | val instr = instrumentState(tailCallEP, firstTempLocal) 126 | 127 | lcode.insert(pc, InsertionPosition.Before, instr.beforeTailCall) 128 | lcode.insert(pc, InsertionPosition.After, instr.afterTailCall(returnValue)) 129 | } 130 | 131 | lcode 132 | } 133 | 134 | } 135 | object InstrumentMethod { 136 | 137 | type BA = (Map[org.opalj.br.PC, AnyRef], List[String]) 138 | 139 | case class Result(original: MethodTemplate, entrypoints: IndexedSeq[METHOD[BA]]) 140 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/InstrumentProject.scala: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation 2 | 3 | import java.io.File 4 | import java.lang.ref.SoftReference 5 | import java.net.URL 6 | 7 | import com.typesafe.config.{ Config => OpalConfig } 8 | import org.opalj.ba.toDA 9 | import org.opalj.bc.Assembler 10 | import org.opalj.br.analyses.Project 11 | import org.opalj.br.{ BaseConfig, ClassFile } 12 | import org.opalj.br.reader.{ BytecodeInstructionsCache, Java8FrameworkWithCaching, Java9FrameworkWithInvokedynamicSupportAndCaching, Java9LibraryFramework } 13 | import org.opalj.log.{ ConsoleOPALLogger, GlobalLogContext, LogContext, OPALLogger } 14 | 15 | object InstrumentProject { 16 | 17 | def apply(p: Project[URL], cfg: Config): ClassDB = 18 | p.allProjectClassFiles.flatMap { cf => InstrumentedClass(cf, p, cfg) }.map { instr => 19 | val name = instr.thisType.toJava 20 | (name -> assemble(instr)) 21 | }.toMap 22 | 23 | def apply(projectFolder: String, cfg: Config): ClassDB = 24 | apply(new File(projectFolder), cfg) 25 | 26 | def apply(projectFolder: File, cfg: Config): ClassDB = 27 | apply(makeProject(Seq(projectFolder), Nil), cfg) 28 | 29 | def apply(projectFolder: File, libraries: Seq[File], cfg: Config): ClassDB = 30 | apply(makeProject(Seq(projectFolder), libraries), cfg) 31 | 32 | def apply(projectFolder: Seq[File], libraries: Seq[File], cfg: Config = Config()): ClassDB = 33 | apply(makeProject(projectFolder, libraries), cfg) 34 | 35 | // TODO use p.parForeachProjectClassFile() 36 | def classFiles(p: Project[URL], resultAlg: Result, cfg: Config = Config()): Unit = 37 | p.projectClassFilesWithSources.foreach { case (cf, url) => 38 | InstrumentedClass(cf, p, cfg).map { instr => 39 | val name = instr.thisType.simpleName 40 | resultAlg(url, name, assemble(instr)) 41 | } 42 | } 43 | 44 | def assemble(cf: ClassFile): Array[Byte] = Assembler(toDA(cf)) 45 | 46 | // (source url of original classfile, simpleName, code) 47 | type Result = (URL, String, Array[Byte]) => Unit 48 | 49 | /** 50 | * Adapted from https://bitbucket.org/delors/opal/src/1dbf23095ea272b50c9daf51d23921f4b2bec4c1/OPAL/br/src/main/scala/org/opalj/br/analyses/Project.scala?at=develop 51 | */ 52 | lazy val JavaLibraryClassFileReader: Java9LibraryFramework.type = Java9LibraryFramework 53 | 54 | private val errorLogger = new ConsoleOPALLogger(true, org.opalj.log.Error) 55 | 56 | OPALLogger.updateLogger(GlobalLogContext, errorLogger) 57 | 58 | def JavaClassFileReader( 59 | theLogContext: LogContext = GlobalLogContext, 60 | theConfig: OpalConfig = BaseConfig 61 | ): Java8FrameworkWithCaching = { 62 | // The following makes use of early initializers 63 | class ConfiguredFramework extends { 64 | override implicit val logContext: LogContext = theLogContext 65 | override implicit val config: OpalConfig = theConfig 66 | } with Java8FrameworkWithCaching(cache) 67 | new ConfiguredFramework 68 | } 69 | 70 | @volatile private[this] var theCache: SoftReference[BytecodeInstructionsCache] = { 71 | new SoftReference(new BytecodeInstructionsCache) 72 | } 73 | private[this] def cache: BytecodeInstructionsCache = { 74 | var cache = theCache.get 75 | if (cache == null) { 76 | this.synchronized { 77 | cache = theCache.get 78 | if (cache == null) { 79 | cache = new BytecodeInstructionsCache 80 | theCache = new SoftReference(cache) 81 | } 82 | } 83 | } 84 | cache 85 | } 86 | 87 | def makeProject(projectFolders: Seq[File], libraries: Seq[File]): Project[URL] = { 88 | val p = Project( 89 | JavaClassFileReader().AllClassFiles(projectFolders), 90 | JavaLibraryClassFileReader.AllClassFiles(libraries), 91 | libraryClassFilesAreInterfacesOnly = true, 92 | virtualClassFiles = Traversable.empty 93 | ) 94 | 95 | OPALLogger.updateLogger(p.logContext, errorLogger) 96 | 97 | p 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/InstrumentationAnalysis.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package instrumentation 3 | 4 | import annotationTypes._ 5 | 6 | import org.opalj.Result 7 | import org.opalj.br._ 8 | import org.opalj.br.cfg.BasicBlock 9 | import org.opalj.br.instructions._ 10 | 11 | /** 12 | * Instrumentation relevant analysis of a method 13 | * 14 | * In contrast to MethodAnalysis, this module focuses on 15 | * instrumentation related information, such as identifying 16 | * entrypoints. 17 | */ 18 | trait InstrumentationAnalysis { 19 | 20 | val ctx: MethodContext 21 | val analyser: MethodAnalysis 22 | 23 | import ctx._ 24 | import analyser._ 25 | 26 | // The local variables that can be used to store temporary results 27 | lazy val firstTempLocal = analyser.firstFreeLocal 28 | 29 | lazy val epPCs = body.flatMap { 30 | case PCAndInstruction(pc, is : MethodInvocationInstruction) if isEffectfulCall(is) => Some(pc) 31 | case _ => None 32 | } 33 | 34 | lazy val entrypoints = epPCs.filterNot(isTailCall).zipWithIndex.map { case (pc, idx) => 35 | EntryPoint(pc, idx + 1) 36 | } 37 | 38 | lazy val needsInstrumentation: Boolean = { 39 | val res = 40 | // 41 | // not abstract or native code 42 | m.body.isDefined && 43 | // 44 | // not flagged as DontInstrument 45 | !m.annotations 46 | .map {_.annotationType} 47 | .contains(DontInstrument.Type) && 48 | // 49 | // not a generated bridge method (TODO maybe we don't need this due to other optimizations) 50 | // !m.hasFlags(ACC_BRIDGE.mask) && 51 | // 52 | // marked as throwing effects 53 | (if (config.optimizePureCalls) 54 | throwsEffects(m) 55 | else 56 | selfOrSuperThrowsEffects(m)) && 57 | // 58 | // at least one EP needs instrumentation 59 | (!config.optimizePureCalls || epPCs.exists(ep => !isTailCall(ep))) 60 | 61 | // println(s"${m.name} needs instrumentation: $res") 62 | res 63 | } 64 | 65 | def isTailCall(ep: PC): Boolean = 66 | nextReturn(next(ep)).isDefined 67 | 68 | def tailcalls: List[PC] = epPCs.filter(isTailCall).toList 69 | 70 | // PCs of return instructions, corresponding to tail effect calls 71 | def tailEffectCallReturns: List[PC] = 72 | epPCs.flatMap(ep => nextReturn(next(ep))).toList 73 | 74 | // returns PC of next return, if between the return PC and the given PC 75 | // are only noops 76 | // used to collect tail-effect-calls 77 | def nextReturn(pc: PC): Option[PC] = { 78 | val instr = instruction(pc) 79 | if (instr.isReturnInstruction) 80 | Some(pc) 81 | else if (instr.isCheckcast) 82 | nextReturn(next(pc)) 83 | else 84 | None 85 | } 86 | 87 | def next(pc: PC): PC = body.pcOfNextInstruction(pc) 88 | def instruction(pc: PC): Instruction = body.instructions(pc) 89 | 90 | 91 | implicit class BasicBlockOps(self: BasicBlock) { 92 | // invariant: if a basic block is an entry point then the last 93 | // instruction is the effectful call. 94 | def isEntryPoint: Boolean = self.entryPointsIn.nonEmpty 95 | 96 | def entryPointsIn: List[PC] = { 97 | var pcs = List.empty[PC] 98 | self foreach { i => if (epPCs contains i) pcs = i :: pcs } 99 | pcs 100 | } 101 | 102 | def isStart: Boolean = self.startPC == 0 103 | } 104 | 105 | def isEffectfulCall(i: MethodInvocationInstruction): Boolean = { 106 | 107 | val callee = getCallee(i) 108 | 109 | // XXX JDK is missing in the classpath, hence the special case 110 | // if (callee.isEmpty && !i.declaringClass.toJava.startsWith("java.")) { 111 | // val name = i.declaringClass.toJava + "." + i.name 112 | // sys error s"Can't determine callee for '$name', maybe the instrumentation analysis doesn't have all necessary class files?" 113 | // } 114 | 115 | callee.exists(selfOrSuperThrowsEffects) 116 | } 117 | 118 | def throwsEffects(m: Method) = 119 | m.exceptionTable.toList 120 | .flatMap {_.exceptions} 121 | .contains {Effects.Type} 122 | 123 | def selfOrSuperThrowsEffects(m: Method) = 124 | throwsEffects(m) || superMethods(m).exists(throwsEffects) 125 | 126 | lazy val cf: ClassFile = m.classFile 127 | 128 | // TODO should be done, once per class, not per method! 129 | lazy val superClasses: Seq[ClassFile] = superClasses(cf) 130 | 131 | private def superClasses(cf: ClassFile): Seq[ClassFile] = 132 | for { 133 | cls <- cf.interfaceTypes.toSeq ++ cf.superclassType.toSeq 134 | cf <- p.classFile(cls) 135 | } yield cf 136 | 137 | // methods potentially overridden by given method 138 | private lazy val superMethods: Seq[Method] = superMethods(m) 139 | 140 | private def superMethods(m: Method): Seq[Method] = 141 | superClasses(m.classFile).flatMap { c => c.findMethod(m.name, m.descriptor) } 142 | 143 | // also see: https://bitbucket.org/snippets/delors/oegdqG/opal-110-resolving-call-targets-of 144 | private def getCallee(i: MethodInvocationInstruction): List[Method] = i match { 145 | case i @ INVOKEVIRTUAL(dc, name, descr) => { 146 | p.instanceCall(m.classFile.thisType, dc, name, descr).toList ++ 147 | p.resolveMethodReference(i).toList 148 | } 149 | 150 | case i @ INVOKESTATIC(dc, isInterface, name, descr) => { 151 | p.staticCall(i).toList 152 | } 153 | 154 | // TODO what about constructor calls (the following is for super-calls) 155 | case INVOKESPECIAL(dc, isInterface, name, descr) => 156 | p.specialCall(dc, m.classFile.thisType, isInterface, name, descr).toList 157 | 158 | case i @ INVOKEINTERFACE(dc, name, descr) => { 159 | p.interfaceCall(dc, name, descr).toList ++ 160 | // since we appearently can't find methods after erasure 161 | p.resolveInterfaceMethodReference(i) 162 | } 163 | 164 | case _ => sys error s"$i is not a function call" 165 | } 166 | 167 | private implicit def result2Option[A](r: Result[A]): Option[A] = 168 | if (r.isEmpty) None else Some(r.value) 169 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/JavaAgent.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package instrumentation 3 | 4 | import java.lang.instrument._ 5 | import java.security.ProtectionDomain 6 | 7 | import InstrumentProject.{ JavaClassFileReader, JavaLibraryClassFileReader } 8 | import org.opalj.br.analyses.Project 9 | import java.net.URL 10 | import java.io._ 11 | import java.nio.file.{ Files, Paths } 12 | 13 | import scala.collection.mutable.WeakHashMap 14 | 15 | object JavaAgent { 16 | def premain(args: String, instr: Instrumentation): Unit = { 17 | log("Installing Effekt JavaAgent") 18 | 19 | log(s"arguments: $args") 20 | 21 | try { 22 | instr.addTransformer(new EffektInstrumentor { 23 | def cfg = Config() 24 | }) 25 | } catch { 26 | case t: Throwable => 27 | println(t) 28 | t.printStackTrace() 29 | } 30 | } 31 | 32 | def agentmain(args: String, instr: Instrumentation): Unit = 33 | premain(args, instr) 34 | 35 | def log(msg: => String): Unit = () 36 | } 37 | 38 | abstract class EffektInstrumentor extends ClassFileTransformer { 39 | 40 | def cfg: Config 41 | 42 | import JavaAgent.log 43 | 44 | type Classpath = String 45 | 46 | val SEP = File.pathSeparatorChar 47 | 48 | private[this] val cache = WeakHashMap.empty[Classpath, Project[URL]] 49 | 50 | // those packages don't use effekt (yet :) ), so we don't need to analyse them for instrumentation 51 | // TODO make this configurable via java agent parameters 52 | lazy val excludeList = Seq( 53 | "com/sun/", 54 | "java/", 55 | "javax/", 56 | "org/apache/", 57 | "sun/", 58 | "jsr166e/", 59 | "com/google/", 60 | "scala/", 61 | "org/opalj/", 62 | "play/", 63 | "org/joda/", 64 | "org/scalatest/", 65 | "org/scalatools/", 66 | "sbt/io/", 67 | "sbt/testing/", 68 | "org/scalactic/", 69 | "org/scalameter/" 70 | ) 71 | 72 | // to speed up start of OPAL exclude the instrumentation jars 73 | // and dependencies from analysis 74 | lazy val excludeJars = Seq( 75 | "/de.opal-project", 76 | "/effekt-instrumentation", 77 | // this is very drastical since everything that is not local will be ignored for now. 78 | "/.ivy2/cache/" 79 | ) 80 | 81 | lazy val reader = JavaClassFileReader() 82 | 83 | override def transform(loader: ClassLoader, className: String, cls: Class[_], domain: ProtectionDomain, data: Array[Byte]): Array[Byte] = try { 84 | 85 | if (className == null) 86 | return null 87 | 88 | if (excludeList exists { e => className startsWith e }) 89 | return null 90 | 91 | val (cf, cfTime) = timed { reader.ClassFile(() => new ByteArrayInputStream(data)).head } 92 | 93 | if (InstrumentedClass shouldBeIgnored cf) 94 | return null 95 | 96 | log(s"instrumenting: $className reading it took ${cfTime}ms") 97 | 98 | // class path is huge. This probably takes ages, even if cached 99 | val cp = System.getProperty("java.class.path") 100 | 101 | // XXX is there a way to speed up the initial loading of OPAL? 102 | // this takes about 8s with full dependencies (which include OPAL itself...) 103 | val baseProject = cache.getOrElseUpdate(cp, { 104 | 105 | val libs = cp.split(SEP).filterNot(f => excludeJars exists { f contains _ }).map(f => new File(f)) 106 | 107 | log(s"Including following paths in static analysis: ${libs mkString "\n"}") 108 | val (p, pTime) = timed { Project( 109 | Nil, // no project sources yet -- we are going to extend this project 110 | JavaLibraryClassFileReader.AllClassFiles(libs), 111 | libraryClassFilesAreInterfacesOnly = true, 112 | virtualClassFiles = Traversable.empty 113 | )} 114 | log(s"Initial project initialization took ${pTime}ms") 115 | p 116 | }) 117 | 118 | // this takes around 1s for every single class! 119 | // - is there a faster way to do this in OPAL? 120 | // - why does OPAL need the URL here? 121 | // for now we just reuse the project 122 | // val (p, pTime) = timed { baseProject.extend(List((cf, new URL("file://" + className)))) } 123 | // log(s"loading classfile into project took ${pTime}ms") 124 | 125 | val (res, resTime) = timed { InstrumentedClass(cf, baseProject, cfg).map { InstrumentProject.assemble } } 126 | 127 | res.map { cf => 128 | dumpClassfile(className + ".before.class", data) 129 | dumpClassfile(className + ".class", cf) 130 | cf 131 | }.orNull 132 | } catch { 133 | case t: Throwable => 134 | println(t) 135 | t.printStackTrace() 136 | null 137 | } 138 | 139 | def dumpClassfile(fileName: String, cf: Array[Byte]) = { 140 | val f = new File("target/instrumented/" + fileName) 141 | f.getParentFile.mkdirs() 142 | val bw = new BufferedOutputStream(new FileOutputStream(f)) 143 | try { bw.write(cf) } finally bw.close() 144 | log("Dumping classfile " + f.getAbsolutePath) 145 | } 146 | 147 | 148 | def timed[R](block: => R): (R, Long) = { 149 | val before = System.currentTimeMillis() 150 | val res = block 151 | val after = System.currentTimeMillis() 152 | (res, after - before) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/MethodAnalysis.scala: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation 2 | 3 | import java.net.URL 4 | 5 | import org.opalj.ai._ 6 | import org.opalj.ai.domain.l0.TypeCheckingDomain 7 | import org.opalj.ai.domain.l1.{ DefaultDomainWithCFGAndDefUse } 8 | import org.opalj.br._ 9 | import org.opalj.br.analyses.Project 10 | import org.opalj.br.instructions._ 11 | import org.opalj.br.cfg.CFGFactory 12 | 13 | /** 14 | * Simple method analysis, extracts locals and operands at given PCs 15 | * 16 | * Potential optimization that could obviously be implemented using 17 | * OPALs current support: 18 | * - don't store statically known constants (or null) 19 | * - don't store aliased references (refId attribute of ReferenceValue) 20 | * - don't store aliased values (through reference equality of domain values) 21 | * 22 | * However, all of this optimizations will only buy us a little bit of 23 | * memory usage and maybe a few load and store instructions. 24 | */ 25 | case class MethodAnalysis(ctx: MethodContext) { outer => 26 | 27 | import ctx._ 28 | 29 | lazy val lastLocal = (Set(0) ++ allLocals).max 30 | 31 | // play it safe since it might be Computational Type 2 32 | // TODO this is strange, since lastLocal should already do that. 33 | lazy val firstFreeLocal = lastLocal + 2 34 | 35 | // can contain null values, if no type yet for a local 36 | def localsAt(n: Int) = aiResult.localsArray(n).map(toFieldType) 37 | def operandsAt(n: Int) = aiResult.operandsArray(n).map(toFieldType) 38 | def aliveAt(n: Int) = aiResult.liveVariables(n).iterator.toArray.toList // instead of code.liveVariables 39 | 40 | // IMPLEMENTATION DETAILS 41 | 42 | // collects all locals that are ever written or read to (including those 43 | // 2nd parts of double and longs) 44 | private lazy val allLocals = body.iterator.collect { 45 | case StoreLocalVariableInstruction(typ, index) => (typ, index) 46 | case LoadLocalVariableInstruction(typ, index) => (typ, index) 47 | }.flatMap { 48 | case (typ, index) if typ.operandSize == 2 => Set(index, index + 1) 49 | case (typ, index) => Set(index) 50 | } 51 | 52 | private lazy val argumentLocals = (0 until m.actualArgumentsCount).toSet 53 | 54 | // some abstract interpretation to know operand stack and locals 55 | val domain = new DefaultDomainWithCFGAndDefUse(p, m) 56 | private lazy val aiResult: AIResult { 57 | val domain: outer.domain.type 58 | } = BaseAI(m, domain) 59 | import domain._ 60 | 61 | lazy val (predecessorPCs, finalPCs, cfJoins) = body.predecessorPCs(p.classHierarchy) 62 | lazy val cfg = bbCFG 63 | 64 | // also look at org.opalj.br.FieldType 65 | // probably better to convert to FieldType! 66 | private def toFieldType(d: DomainValue): FieldType = d.fieldType 67 | 68 | implicit class DomainValueOps(self: DomainValue) { 69 | def size: Int = compType.operandSize 70 | 71 | def compType: ComputationalType = 72 | if (self == null || self == TheIllegalValue) 73 | null 74 | else self.computationalType 75 | 76 | def fieldType: FieldType = compType match { 77 | case ComputationalTypeDouble => DoubleType 78 | case ComputationalTypeFloat => FloatType 79 | case ComputationalTypeInt => IntegerType 80 | case ComputationalTypeLong => LongType 81 | case ComputationalTypeReference => self.asDomainReferenceValue.valueType.get 82 | 83 | // ??? ReturnAdresses, e.g. effectful finally clauses ??? 84 | case null => null 85 | } 86 | 87 | def aliases(other: DomainValue) = (self, other) match { 88 | case (a : ReferenceValue, b : ReferenceValue) => a.refId == b.refId 89 | case (a : IntegerRange, b : IntegerRange) => a eq b 90 | case _ => false 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/MethodContext.scala: -------------------------------------------------------------------------------- 1 | package effekt.instrumentation 2 | 3 | import org.opalj.br.{ Method, Code } 4 | import org.opalj.br.analyses.Project 5 | import java.net.URL 6 | 7 | case class MethodContext( 8 | m: Method, 9 | p: Project[URL], 10 | fresh: FreshNames = FreshNames(), 11 | config: Config 12 | ) { 13 | implicit lazy val body: Code = m.body.get 14 | } -------------------------------------------------------------------------------- /instrumentation/src/main/scala/effekt/instrumentation/package.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | 3 | import java.io.{ ByteArrayInputStream, InputStream } 4 | 5 | import org.opalj.ba.CodeElement 6 | import org.opalj.br._ 7 | import org.opalj.br.instructions._ 8 | import org.opalj.da.ClassFileReader 9 | 10 | package object instrumentation { 11 | 12 | type ClassDB = Map[String, Array[Byte]] 13 | 14 | object annotationTypes { 15 | object AlreadyInstrumented { 16 | val Type = ObjectType("effekt/instrumentation/annotations/AlreadyInstrumented") 17 | } 18 | object DontInstrument { 19 | val Type = ObjectType("effekt/instrumentation/annotations/DontInstrument") 20 | } 21 | } 22 | 23 | object Effects { 24 | val Type = ObjectType("effekt/Effects") 25 | 26 | def throwReturn = code(GETSTATIC(Type, "RETURN", Type), ATHROW) 27 | } 28 | 29 | // UTILS 30 | def debug(msg: String) = println(msg) 31 | 32 | def code(els: CodeElement[AnyRef]*) = els 33 | 34 | def dump(input: => InputStream, name: String): Unit = 35 | org.opalj.io.writeAndOpen( 36 | ClassFileReader.ClassFile(() => input).head.toXHTML(None), 37 | name, ".html") 38 | 39 | def dump(bytecode: Array[Byte], name: String): Unit = 40 | dump(new ByteArrayInputStream(bytecode), name) 41 | 42 | // load and return a default value of given type 43 | def returnDefaultValue(typ: Type) = typ match { 44 | case VoidType => 45 | code(RETURN) 46 | case ft: FieldType => 47 | code(LoadConstantInstruction.defaultValue(ft), ReturnInstruction(ft)) 48 | } 49 | 50 | import org.opalj.collection.immutable.{ RefArray => RA } 51 | // re-export 52 | type RefArray[T] = RA[T] 53 | object RefArray { 54 | def apply[T <: AnyRef](vs: T*): RefArray[T] = RA.mapFrom(vs)(identity) 55 | def empty = RA.empty 56 | } 57 | 58 | implicit class SeqOps[T <: AnyRef](seq: Seq[T]) { 59 | // converts between a scala Seq and an OPAL RefArray 60 | def toRefArray = RA.mapFrom(seq)(identity) 61 | } 62 | } -------------------------------------------------------------------------------- /project/assembly.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.0.3 2 | -------------------------------------------------------------------------------- /sbtplugin/src/main/scala/EffektPlugin.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package plugin 3 | 4 | import effekt.instrumentation._ 5 | import sbt._ 6 | import sbt.io.Path 7 | import sbt.Keys._ 8 | import java.io.File 9 | import org.opalj.br.analyses.Project 10 | 11 | object EffektPlugin extends AutoPlugin { 12 | 13 | object autoImport extends BaseEffektKeys 14 | import autoImport._ 15 | 16 | def effektDefaultSettings: Seq[Setting[_]] = Seq( 17 | effektClassDirectory := (classDirectory in Compile).value, 18 | 19 | effektTargetDirectory := effektClassDirectory.value, 20 | 21 | effektDependencyClassPath := (dependencyClasspath in Compile).value.map { 22 | cp => cp.data 23 | }, 24 | 25 | effektInstrument := { 26 | 27 | val logger = sLog.value 28 | 29 | logger.info("Running instrumentation") 30 | 31 | 32 | val input = effektClassDirectory.value 33 | val output = effektTargetDirectory.value 34 | val dependencies = effektDependencyClassPath.value 35 | 36 | val p = InstrumentProject.makeProject(Seq(input), dependencies) 37 | 38 | InstrumentProject.classFiles(p, { case (url, name, code) => 39 | val classFile = targetClassFile(url, name) 40 | logger.info(s"Generated code for ${name}") 41 | IO.write(classFile, code) 42 | }) 43 | 44 | logger.info("Completed instrumentation") 45 | 46 | /** 47 | * Computes the new classfile path in `output` from the old 48 | * full path `source` and the new (unqualified) classname `simpleName`. 49 | */ 50 | def targetClassFile(source: java.net.URL, simpleName: String): File = 51 | new File(basepathInTarget(source), s"$simpleName.class") 52 | 53 | /** 54 | * /some/input/foo/bar.txt 55 | * -> 56 | * /other/output/foo 57 | */ 58 | def basepathInTarget(source: java.net.URL): File = { 59 | val sourceFile = IO.asFile(source) 60 | val relative = IO.relativizeFile(input, sourceFile) 61 | .getOrElse(sys error s"can't find relative paths to ${sourceFile}") 62 | 63 | val path = relative.toPath 64 | 65 | if (path.getParent == null) 66 | output 67 | else 68 | output / path.getParent.toString 69 | } 70 | } 71 | ) 72 | 73 | def instrumentAfterCompile: Seq[Setting[_]] = Seq( 74 | // standard settings: let effektInstrument depend on compile 75 | effektInstrument in Compile := (effektInstrument in Compile).dependsOn(compile in Compile).value, 76 | 77 | // let run and publish depend on instrument 78 | products in Compile := (products in Compile).dependsOn(effektInstrument in Compile).value 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /sbtplugin/src/main/scala/Keys.scala: -------------------------------------------------------------------------------- 1 | package effekt 2 | package plugin 3 | 4 | import sbt._ 5 | import java.io.File 6 | 7 | object EffektKeys extends BaseEffektKeys 8 | 9 | class BaseEffektKeys { 10 | final val effektClassDirectory = taskKey[File]("The folder containing the classfiles to be instrumented") 11 | final val effektTargetDirectory = taskKey[File]("The folder the instrumented classfiles should be written to") 12 | final val effektDependencyClassPath = taskKey[Seq[File]]("Paths to dependency jar files that are not going to be instrumented but necessary for analysis") 13 | final val effektInstrument = taskKey[Unit]("Perform instrumentation on compiled class files") 14 | } --------------------------------------------------------------------------------