|
| 1 | +import collection.mutable |
| 2 | + |
| 3 | +/** A framework for defining stackable entry point wrappers */ |
| 4 | +object EntryPoint: |
| 5 | + |
| 6 | + /** A base trait for wrappers of entry points. |
| 7 | + * Sub-traits: Annotation#Wrapper |
| 8 | + * Adapter#Wrapper |
| 9 | + */ |
| 10 | + sealed trait Wrapper |
| 11 | + |
| 12 | + /** This class provides a framework for compiler-generated wrappers |
| 13 | + * of "entry-point" methods. It routes and transforms parameters and results |
| 14 | + * between a compiler-generated wrapper method that has calling conventions |
| 15 | + * fixed by a framework and a user-written entry-point method that can have |
| 16 | + * flexible argument lists. It allows the wrapper to provide help and usage |
| 17 | + * information as well as customised error messages if the actual wrapper arguments |
| 18 | + * do not match the expected entry-point parameters. |
| 19 | + * |
| 20 | + * The protocol of calls from the wrapper method is as follows: |
| 21 | + * |
| 22 | + * 1. Create a `call` instance with the wrapper argument. |
| 23 | + * 2. For each parameter of the entry-point, invoke `call.nextArgGetter`, |
| 24 | + * or `call.finalArgsGetter` if is a final varargs parameter. |
| 25 | + * 3. Invoke `call.run` with the closure of entry-point applied to all arguments. |
| 26 | + * |
| 27 | + * The wrapper class has this outline: |
| 28 | + * |
| 29 | + * object <wrapperClass>: |
| 30 | + * @WrapperAnnotation def <wrapperMethod>(args: <Argument>) = |
| 31 | + * ... |
| 32 | + * |
| 33 | + * Here `<wrapperClass>` and `<wrapperMethod>` are obtained from an |
| 34 | + * inline call to the `wrapperName` method. |
| 35 | + */ |
| 36 | + trait Annotation extends annotation.StaticAnnotation: |
| 37 | + |
| 38 | + /** The class used for argument parsing. E.g. `scala.util.FromString`, if |
| 39 | + * arguments are strings, but it could be something else. |
| 40 | + */ |
| 41 | + type ArgumentParser[T] |
| 42 | + |
| 43 | + /** The required result type of the user-defined main function */ |
| 44 | + type EntryPointResult |
| 45 | + |
| 46 | + /** The fully qualified name (relative to enclosing package) to |
| 47 | + * use for the static wrapper method. |
| 48 | + * @param entryPointName the fully qualified name of the user-defined entry point method |
| 49 | + */ |
| 50 | + inline def wrapperName(entryPointName: String): String |
| 51 | + |
| 52 | + /** Create an entry point wrapper. |
| 53 | + * @param entryPointName the fully qualified name of the user-defined entry point method |
| 54 | + * @param docComment the doc comment of the user-defined entry point method |
| 55 | + */ |
| 56 | + def wrapper(entryPointName: String, docComment: String): Wrapper |
| 57 | + |
| 58 | + /** Base class for descriptions of an entry point wrappers */ |
| 59 | + abstract class Wrapper extends EntryPoint.Wrapper: |
| 60 | + |
| 61 | + /** The type of the wrapper argument. E.g., for Java main methods: `Array[String]` */ |
| 62 | + type Argument |
| 63 | + |
| 64 | + /** The return type of the generated wrapper. E.g., for Java main methods: `Unit` */ |
| 65 | + type Result |
| 66 | + |
| 67 | + /** An annotation type with which the wrapper method is decorated. |
| 68 | + * No annotation is generated if the type is left abstract. |
| 69 | + * Multiple annotations are generated if the type is an intersection of annotations. |
| 70 | + */ |
| 71 | + type WrapperAnnotation <: annotation.Annotation |
| 72 | + |
| 73 | + /** The fully qualified name of the user-defined entry point method that is wrapped */ |
| 74 | + val entryPointName: String |
| 75 | + |
| 76 | + /** The doc comment of the user-defined entry point method that is wrapped */ |
| 77 | + val docComment: String |
| 78 | + |
| 79 | + /** A new wrapper call with arguments from `args` */ |
| 80 | + def call(arg: Argument): Call |
| 81 | + |
| 82 | + /** A class representing a wrapper call */ |
| 83 | + abstract class Call: |
| 84 | + |
| 85 | + /** The getter for the next argument of type `T` */ |
| 86 | + def nextArgGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T |
| 87 | + |
| 88 | + /** The getter for a final varargs argument of type `T*` */ |
| 89 | + def finalArgsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] |
| 90 | + |
| 91 | + /** Run `entryPointWithArgs` if all arguments are valid, |
| 92 | + * or print usage information and/or error messages. |
| 93 | + * @param entryPointWithArgs the applied entry-point to run |
| 94 | + */ |
| 95 | + def run(entryPointWithArgs: => EntryPointResult): Result |
| 96 | + end Call |
| 97 | + end Wrapper |
| 98 | + end Annotation |
| 99 | + |
| 100 | + /** An annotation that generates an adapter of an entry point wrapper. |
| 101 | + * An `EntryPoint.Adapter` annotation should always be written together |
| 102 | + * with an `EntryPoint.Annotation` and the adapter should be given first. |
| 103 | + * If several adapters are present, they are applied right to left. |
| 104 | + * Example: |
| 105 | + * |
| 106 | + * @logged @transactional @main def f(...) |
| 107 | + * |
| 108 | + * This wraps the main method generated by @main first in a `transactional` |
| 109 | + * wrapper and then in a `logged` wrapper. The result would look like this: |
| 110 | + * |
| 111 | + * $logged$wrapper.adapt { y => |
| 112 | + * $transactional$wrapper.adapt { z => |
| 113 | + * val cll = $main$wrapper.call(z) |
| 114 | + * val arg1 = ... |
| 115 | + * ... |
| 116 | + * val argN = ... |
| 117 | + * cll.run(...) |
| 118 | + * } (y) |
| 119 | + * } (x) |
| 120 | + * |
| 121 | + * where |
| 122 | + * |
| 123 | + * - $logged$wrapper, $transactional$wrapper, $main$wrapper are the wrappers |
| 124 | + * created from @logged, @transactional, and @main, respectively. |
| 125 | + * - `x` is the argument of the outer $logged$wrapper. |
| 126 | + */ |
| 127 | + trait Adapter extends annotation.StaticAnnotation: |
| 128 | + |
| 129 | + /** Creates a new wrapper around `wrapped` */ |
| 130 | + def wrapper(wrapped: EntryPoint.Wrapper): Wrapper |
| 131 | + |
| 132 | + /** The wrapper class. A wrapper class must define a method `adapt` |
| 133 | + * that maps unary functions to unary functions. A typical definition |
| 134 | + * of `adapt` is: |
| 135 | + * |
| 136 | + * def adapt(f: A1 => B1)(a: A2): B2 = toB2(f(toA1(a))) |
| 137 | + * |
| 138 | + * This version of `adapt` converts its argument `a` to the wrapped |
| 139 | + * function's argument type `A1`, applies the function, and converts |
| 140 | + * the application's result back to type `B2`. `adapt` can also call |
| 141 | + * the wrapped function only under a condition or call it multiple times. |
| 142 | + * |
| 143 | + * `adapt` can also be polymorphic. For instance: |
| 144 | + * |
| 145 | + * def adapt[R](f: A1 => R)(a: A2): R = f(toA1(a)) |
| 146 | + * |
| 147 | + * or |
| 148 | + * |
| 149 | + * def adapt[A, R](f: A => R)(a: A): R = { log(a); f(a) } |
| 150 | + * |
| 151 | + * Since `adapt` can be of many forms, the base class does not provide |
| 152 | + * an abstract method that needs to be implemented in concrete wrapper |
| 153 | + * classes. Instead it is checked that the types line up when adapt chains |
| 154 | + * are assembled. |
| 155 | + * |
| 156 | + * I investigated an explicitly typed approach, but could not arrive at a |
| 157 | + * solution that was clean and simple enough. If Scala had more dependent |
| 158 | + * type support, it would be quite straightforward, i.e. generic `Wrapper` |
| 159 | + * could be defined like this: |
| 160 | + * |
| 161 | + * class Wrapper(wrapped: EntryPoint.Wrapper): |
| 162 | + * type Argument |
| 163 | + * type Result |
| 164 | + * def adapt(f: wrapped.Argument => wrapped.Result)(x: Argument): Result |
| 165 | + * |
| 166 | + * But to get this to work, we'd need support for types depending on their |
| 167 | + * arguments, e.g. a type of the form `Wrapper(wrapped)`. That's an interesting |
| 168 | + * avenue to pursue. Until that materializes I think it's preferable to |
| 169 | + * keep the `adapt` type contract implicit (types are still checked when adapts |
| 170 | + * are generated, of course). |
| 171 | + */ |
| 172 | + abstract class Wrapper extends EntryPoint.Wrapper: |
| 173 | + /** The wrapper that this wrapped in turn by this wrapper */ |
| 174 | + val wrapped: EntryPoint.Wrapper |
| 175 | + |
| 176 | + /** The wrapper of the entry point annotation that this wrapper |
| 177 | + * wraps directly or indirectly |
| 178 | + */ |
| 179 | + def finalWrapped: EntryPoint.Annotation#Wrapper = wrapped match |
| 180 | + case wrapped: EntryPoint.Adapter#Wrapper => wrapped.finalWrapped |
| 181 | + case wrapped: EntryPoint.Annotation#Wrapper => wrapped |
| 182 | + end Wrapper |
| 183 | + end Adapter |
0 commit comments