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