Skip to content

Commit a929be5

Browse files
committed
Add customization of wrapper class generation
1 parent 08a42b2 commit a929be5

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import annotation.StaticAnnotation
2+
import collection.mutable
3+
4+
/** MainAnnotation provides the functionality for a compiler-generated wrapper class.
5+
* It links a compiler-generated main method (call it compiler-main) to a user
6+
* written main method (user-main).
7+
* The protocol of calls from compiler-main is as follows:
8+
*
9+
* - create a `command` with the command line arguments,
10+
* - for each parameter of user-main, a call to `command.argGetter`,
11+
* or `command.argsGetter` if is a final varargs parameter,
12+
* - a call to `command.run` with the closure of user-main applied to all arguments.
13+
*
14+
* The wrapper class has this outline:
15+
*
16+
* object <wrapperClass>:
17+
* def <wrapperMethod>(args: <CommandLineArgs>) =
18+
* ...
19+
*
20+
* Here the `<wrapperClass>` name is the result of an inline call to `wrapperClassName`
21+
* and `<wrapperMethod>` is the result of an inline call to `wrapperMethodName`
22+
*
23+
* The result type of `<main>` is the same as the result type of `run`
24+
* in the concrete implementation of `MainAnnotation`.
25+
*/
26+
trait MainAnnotation extends StaticAnnotation:
27+
28+
/** The class used for argument string parsing. E.g. `scala.util.FromString`,
29+
* but could be something else
30+
*/
31+
type ArgumentParser[T]
32+
33+
/** The required result type of the user-defined main function */
34+
type MainResultType
35+
36+
/** The type of the command line arguments. E.g., for Java main methods: `Array[String]` */
37+
type CommandLineArgs
38+
39+
/** The return type of the generated command. E.g., for Java main methods: `Unit` */
40+
type CommandResult
41+
42+
/** A new command with arguments from `args` */
43+
def command(args: CommandLineArgs): Command
44+
45+
/** A class representing a command to run */
46+
abstract class Command:
47+
48+
/** The getter for the next argument of type `T` */
49+
def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T
50+
51+
/** The getter for a final varargs argument of type `T*` */
52+
def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T]
53+
54+
/** Run `program` if all arguments are valid,
55+
* or print usage information and/or error messages.
56+
* @param program the program to run
57+
* @param mainName the fully qualified name of the user-defined main method
58+
* @param docComment the doc comment of the user-defined main method
59+
*/
60+
def run(program: => MainResultType, mainName: String, docComment: String): CommandResult
61+
end Command
62+
63+
// Compile-time abstractions to control wrapper class:
64+
65+
/** The name to use for the static wrapper class
66+
* @param mainName the fully qualified name of the user-defined main method
67+
*/
68+
inline def wrapperClassName(mainName: String): String
69+
70+
/** The name to use for the main method in the static wrapper class
71+
* @param mainName the fully qualified name of the user-defined main method
72+
*/
73+
inline def wrapperMethodName(mainName: String): String
74+
75+
end MainAnnotation
76+
77+
//Sample main class, can be freely implemented:
78+
79+
class main extends MainAnnotation:
80+
81+
type ArgumentParser[T] = util.FromString[T]
82+
type MainResultType = Any
83+
type CommandLineArgs = Array[String]
84+
type CommandResult = Unit
85+
86+
def command(args: Array[String]) = new Command:
87+
88+
/** A buffer of demanded argument names, plus
89+
* "?" if it has a default
90+
* "*" if it is a vararg
91+
* "" otherwise
92+
*/
93+
private var argInfos = new mutable.ListBuffer[(String, String)]
94+
95+
/** A buffer for all errors */
96+
private var errors = new mutable.ListBuffer[String]
97+
98+
/** Issue an error, and return an uncallable getter */
99+
private def error(msg: String): () => Nothing =
100+
errors += msg
101+
() => assertFail("trying to get invalid argument")
102+
103+
/** The next argument index */
104+
private var argIdx: Int = 0
105+
106+
private def argAt(idx: Int): Option[String] =
107+
if idx < args.length then Some(args(idx)) else None
108+
109+
private def nextPositionalArg(): Option[String] =
110+
while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2
111+
val result = argAt(argIdx)
112+
argIdx += 1
113+
result
114+
115+
private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T =
116+
p.fromStringOption(arg) match
117+
case Some(t) => () => t
118+
case None => error(s"invalid argument for $argName: $arg")
119+
120+
def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T =
121+
argInfos += ((argName, if defaultValue.isDefined then "?" else ""))
122+
val idx = args.indexOf(s"--$argName")
123+
val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg()
124+
argOpt match
125+
case Some(arg) => convert(argName, arg, p)
126+
case None => defaultValue match
127+
case Some(t) => () => t
128+
case None => error(s"missing argument for $argName")
129+
130+
def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] =
131+
argInfos += ((argName, "*"))
132+
def remainingArgGetters(): List[() => T] = nextPositionalArg() match
133+
case Some(arg) => convert(arg, argName, p) :: remainingArgGetters()
134+
case None => Nil
135+
val getters = remainingArgGetters()
136+
() => getters.map(_())
137+
138+
def run(f: => MainResultType, mainName: String, docComment: String): Unit =
139+
def usage(): Unit =
140+
println(s"Usage: java ${wrapperClassName(mainName)} ${argInfos.map(_ + _).mkString(" ")}")
141+
142+
def explain(): Unit =
143+
if docComment.nonEmpty then println(docComment) // todo: process & format doc comment
144+
145+
def flagUnused(): Unit = nextPositionalArg() match
146+
case Some(arg) =>
147+
error(s"unused argument: $arg")
148+
flagUnused()
149+
case None =>
150+
for
151+
arg <- args
152+
if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2))
153+
do
154+
error(s"unknown argument name: $arg")
155+
end flagUnused
156+
157+
if args.isEmpty || args.contains("--help") then
158+
usage()
159+
explain()
160+
else
161+
flagUnused()
162+
if errors.nonEmpty then
163+
for msg <- errors do println(s"Error: $msg")
164+
usage()
165+
else f match
166+
case n: Int if n < 0 => System.exit(-n)
167+
case _ =>
168+
end run
169+
end command
170+
171+
inline def wrapperClassName(mainName: String): String =
172+
mainName.drop(mainName.lastIndexOf('.') + 1)
173+
174+
inline def wrapperMethodName(mainName: String): String = "main"
175+
176+
end main
177+
178+
// Sample main method
179+
180+
object myProgram:
181+
182+
/** Adds two numbers */
183+
@main def add(num: Int, inc: Int = 1): Unit =
184+
println(s"$num + $inc = ${num + inc}")
185+
186+
end myProgram
187+
188+
// Compiler generated code:
189+
190+
object add extends main:
191+
def main(args: Array[String]) =
192+
val cmd = command(args)
193+
val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]])
194+
val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1))
195+
cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers")
196+
end add
197+
198+
/** --- Some scenarios ----------------------------------------
199+
200+
> java add 2 3
201+
2 + 3 = 5
202+
> java add 4
203+
4 + 1 = 5
204+
> java add --num 10 --inc -2
205+
10 + -2 = 8
206+
> java add --num 10
207+
10 + 1 = 11
208+
> java add --help
209+
Usage: java add num inc?
210+
Adds two numbers
211+
> java add
212+
Usage: java add num inc?
213+
Adds two numbers
214+
> java add 1 2 3 4
215+
Error: unused argument: 3
216+
Error: unused argument: 4
217+
Usage: java add num inc?
218+
> java add -n 1 -i 10
219+
Error: invalid argument for num: -n
220+
Error: unused argument: -i
221+
Error: unused argument: 10
222+
Usage: java add num inc?
223+
> java add --n 1 --i 10
224+
Error: missing argument for num
225+
Error: unknown argument name: --n
226+
Error: unknown argument name: --i
227+
Usage: java add num inc?
228+
> java add true 10
229+
Error: invalid argument for num: true
230+
Usage: java add num inc?
231+
> java add true false
232+
Error: invalid argument for num: true
233+
Error: invalid argument for inc: false
234+
Usage: java add num inc?
235+
> java add true false 10
236+
Error: invalid argument for num: true
237+
Error: invalid argument for inc: false
238+
Error: unused argument: 10
239+
Usage: java add num inc?
240+
> java add --inc 10 --num 20
241+
20 + 10 = 30
242+
> java add binary 10 01
243+
Error: invalid argument for num: binary
244+
Error: unused argument: 01
245+
Usage: java add num inc?
246+
247+
*/

0 commit comments

Comments
 (0)