Skip to content

Commit a323ed4

Browse files
committed
Prototype for stackable entry point wrappers
1 parent cd9747d commit a323ed4

File tree

7 files changed

+478
-0
lines changed

7 files changed

+478
-0
lines changed

tests/pos/decorators/DocComment.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/** Represents a doc comment, splitting it into `body` and `tags`
2+
* `tags` are all lines starting with an `@`, where the tag thats starts
3+
* with `@` is paired with the text that follows, up to the next
4+
* tagged line.
5+
* `body` what comes before the first tagged line
6+
*/
7+
case class DocComment(body: String, tags: Map[String, List[String]])
8+
object DocComment:
9+
def fromString(str: String): DocComment =
10+
val lines = str.linesIterator.toList
11+
def tagged(line: String): Option[(String, String)] =
12+
val ws = WordSplitter(line)
13+
val tag = ws.next()
14+
if tag.startsWith("@") then Some(tag, line.drop(ws.nextOffset))
15+
else None
16+
val (bodyLines, taggedLines) = lines.span(tagged(_).isEmpty)
17+
def tagPairs(lines: List[String]): List[(String, String)] = lines match
18+
case line :: lines1 =>
19+
val (tag, descPrefix) = tagged(line).get
20+
val (untaggedLines, lines2) = lines1.span(tagged(_).isEmpty)
21+
val following = untaggedLines.map(_.dropWhile(_ <= ' '))
22+
(tag, (descPrefix :: following).mkString("\n")) :: tagPairs(lines2)
23+
case _ =>
24+
Nil
25+
DocComment(bodyLines.mkString("\n"), tagPairs(taggedLines).groupMap(_._1)(_._2))
26+
end DocComment

tests/pos/decorators/EntryPoint.scala

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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

tests/pos/decorators/Test.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
object Test:
2+
def main(args: Array[String]) =
3+
def testAdd(args: String) =
4+
println(s"> java add $args")
5+
add.main(args.split(" "))
6+
println()
7+
def testAddAll(args: String) =
8+
println(s"> java addAll $args")
9+
addAll.main(args.split(" "))
10+
println()
11+
12+
testAdd("2 3")
13+
testAdd("4")
14+
testAdd("--num 10 --inc -2")
15+
testAdd("--num 10")
16+
testAdd("--help")
17+
testAdd("")
18+
testAdd("1 2 3 4")
19+
testAdd("-n 1 -i 2")
20+
testAdd("true 10")
21+
testAdd("true false")
22+
testAdd("true false 10")
23+
testAdd("--inc 10 --num 20")
24+
testAdd("binary 10 01")
25+
testAddAll("1 2 3 4 5")
26+
testAddAll("--nums")
27+
testAddAll("--nums 33 44")
28+
testAddAll("true 1 2 3")
29+
testAddAll("--help")
30+
end Test
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/** An iterator to return words in a string while keeping tarck of their offsets */
2+
class WordSplitter(str: String, start: Int = 0, isSeparator: Char => Boolean = _ <= ' ')
3+
extends Iterator[String]:
4+
private var idx: Int = start
5+
private var lastIdx: Int = start
6+
private var word: String = _
7+
8+
private def skipSeparators() =
9+
while idx < str.length && isSeparator(str(idx)) do
10+
idx += 1
11+
12+
def lastWord = word
13+
def lastOffset = lastIdx
14+
15+
def nextOffset =
16+
skipSeparators()
17+
idx
18+
19+
def next(): String =
20+
skipSeparators()
21+
lastIdx = idx
22+
val b = new StringBuilder
23+
while idx < str.length && !isSeparator(str(idx)) do
24+
b += str(idx)
25+
idx += 1
26+
word = b.toString
27+
word
28+
29+
def hasNext: Boolean = nextOffset < str.length
30+
end WordSplitter

0 commit comments

Comments
 (0)