Skip to content

Commit 11760ea

Browse files
authored
Merge pull request #8776 from dotty-staging/add-main-proto
Prototype for proposed main method generation scheme
2 parents ec8d439 + 165dc16 commit 11760ea

11 files changed

+942
-3
lines changed

compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,17 @@ object TypeTestsCasts {
220220
* can be true in some cases. Issues a warning or an error otherwise.
221221
*/
222222
def checkSensical(foundClasses: List[Symbol])(using Context): Boolean =
223+
def exprType = i"type ${expr.tpe.widen.stripAnnots}"
223224
def check(foundCls: Symbol): Boolean =
224225
if (!isCheckable(foundCls)) true
225226
else if (!foundCls.derivesFrom(testCls)) {
226227
val unrelated = !testCls.derivesFrom(foundCls) && (
227228
testCls.is(Final) || !testCls.is(Trait) && !foundCls.is(Trait)
228229
)
229230
if (foundCls.is(Final))
230-
unreachable(i"type ${expr.tpe.widen} is not a subclass of $testCls")
231+
unreachable(i"$exprType is not a subclass of $testCls")
231232
else if (unrelated)
232-
unreachable(i"type ${expr.tpe.widen} and $testCls are unrelated")
233+
unreachable(i"$exprType and $testCls are unrelated")
233234
else true
234235
}
235236
else true
@@ -238,7 +239,7 @@ object TypeTestsCasts {
238239
val foundEffectiveClass = effectiveClass(expr.tpe.widen)
239240

240241
if foundEffectiveClass.isPrimitiveValueClass && !testCls.isPrimitiveValueClass then
241-
ctx.error("cannot test if value types are references", tree.sourcePos)
242+
ctx.error(i"cannot test if value of $exprType is a reference of $testCls", tree.sourcePos)
242243
false
243244
else foundClasses.exists(check)
244245
end checkSensical
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import annotation.StaticAnnotation
2+
import collection.mutable
3+
4+
/** MainAnnotation provides the functionality for a compiler-generated main 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+
trait MainAnnotation extends StaticAnnotation:
15+
16+
/** The class used for argument string parsing. E.g. `scala.util.FromString`,
17+
* but could be something else
18+
*/
19+
type ArgumentParser[T]
20+
21+
/** The required result type of the main function */
22+
type MainResultType
23+
24+
/** A new command with arguments from `args` */
25+
def command(args: Array[String]): Command
26+
27+
/** A class representing a command to run */
28+
abstract class Command:
29+
30+
/** The getter for the next argument of type `T` */
31+
def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T
32+
33+
/** The getter for a final varargs argument of type `T*` */
34+
def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T]
35+
36+
/** Run `program` if all arguments are valid,
37+
* or print usage information and/or error messages.
38+
*/
39+
def run(program: => MainResultType, progName: String, docComment: String): Unit
40+
end Command
41+
end MainAnnotation
42+
43+
//Sample main class, can be freely implemented:
44+
45+
class main extends MainAnnotation:
46+
47+
type ArgumentParser[T] = util.FromString[T]
48+
type MainResultType = Any
49+
50+
def command(args: Array[String]): Command = new Command:
51+
52+
/** A buffer of demanded argument names, plus
53+
* "?" if it has a default
54+
* "*" if it is a vararg
55+
* "" otherwise
56+
*/
57+
private var argInfos = new mutable.ListBuffer[(String, String)]
58+
59+
/** A buffer for all errors */
60+
private var errors = new mutable.ListBuffer[String]
61+
62+
/** Issue an error, and return an uncallable getter */
63+
private def error(msg: String): () => Nothing =
64+
errors += msg
65+
() => assertFail("trying to get invalid argument")
66+
67+
/** The next argument index */
68+
private var argIdx: Int = 0
69+
70+
private def argAt(idx: Int): Option[String] =
71+
if idx < args.length then Some(args(idx)) else None
72+
73+
private def nextPositionalArg(): Option[String] =
74+
while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2
75+
val result = argAt(argIdx)
76+
argIdx += 1
77+
result
78+
79+
private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T =
80+
p.fromStringOption(arg) match
81+
case Some(t) => () => t
82+
case None => error(s"invalid argument for $argName: $arg")
83+
84+
def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T =
85+
argInfos += ((argName, if defaultValue.isDefined then "?" else ""))
86+
val idx = args.indexOf(s"--$argName")
87+
val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg()
88+
argOpt match
89+
case Some(arg) => convert(argName, arg, p)
90+
case None => defaultValue match
91+
case Some(t) => () => t
92+
case None => error(s"missing argument for $argName")
93+
94+
def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] =
95+
argInfos += ((argName, "*"))
96+
def remainingArgGetters(): List[() => T] = nextPositionalArg() match
97+
case Some(arg) => convert(arg, argName, p) :: remainingArgGetters()
98+
case None => Nil
99+
val getters = remainingArgGetters()
100+
() => getters.map(_())
101+
102+
def run(f: => MainResultType, progName: String, docComment: String): Unit =
103+
def usage(): Unit =
104+
println(s"Usage: $progName ${argInfos.map(_ + _).mkString(" ")}")
105+
106+
def explain(): Unit =
107+
if docComment.nonEmpty then println(docComment) // todo: process & format doc comment
108+
109+
def flagUnused(): Unit = nextPositionalArg() match
110+
case Some(arg) =>
111+
error(s"unused argument: $arg")
112+
flagUnused()
113+
case None =>
114+
for
115+
arg <- args
116+
if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2))
117+
do
118+
error(s"unknown argument name: $arg")
119+
end flagUnused
120+
121+
if args.isEmpty || args.contains("--help") then
122+
usage()
123+
explain()
124+
else
125+
flagUnused()
126+
if errors.nonEmpty then
127+
for msg <- errors do println(s"Error: $msg")
128+
usage()
129+
else f match
130+
case n: Int if n < 0 => System.exit(-n)
131+
case _ =>
132+
end run
133+
end command
134+
end main
135+
136+
// Sample main method
137+
138+
object myProgram:
139+
140+
/** Adds two numbers */
141+
@main def add(num: Int, inc: Int = 1): Unit =
142+
println(s"$num + $inc = ${num + inc}")
143+
144+
end myProgram
145+
146+
// Compiler generated code:
147+
148+
object add extends main:
149+
def main(args: Array[String]) =
150+
val cmd = command(args)
151+
val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]])
152+
val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1))
153+
cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers")
154+
end add
155+
156+
/** --- Some scenarios ----------------------------------------
157+
158+
> java add 2 3
159+
2 + 3 = 5
160+
> java add 4
161+
4 + 1 = 5
162+
> java add --num 10 --inc -2
163+
10 + -2 = 8
164+
> java add --num 10
165+
10 + 1 = 11
166+
> java add --help
167+
Usage: add num inc?
168+
Adds two numbers
169+
> java add
170+
Usage: add num inc?
171+
Adds two numbers
172+
> java add 1 2 3 4
173+
Error: unused argument: 3
174+
Error: unused argument: 4
175+
Usage: add num inc?
176+
> java add -n 1 -i 10
177+
Error: invalid argument for num: -n
178+
Error: unused argument: -i
179+
Error: unused argument: 10
180+
Usage: add num inc?
181+
> java add --n 1 --i 10
182+
Error: missing argument for num
183+
Error: unknown argument name: --n
184+
Error: unknown argument name: --i
185+
Usage: add num inc?
186+
> java add true 10
187+
Error: invalid argument for num: true
188+
Usage: add num inc?
189+
> java add true false
190+
Error: invalid argument for num: true
191+
Error: invalid argument for inc: false
192+
Usage: add num inc?
193+
> java add true false 10
194+
Error: invalid argument for num: true
195+
Error: invalid argument for inc: false
196+
Error: unused argument: 10
197+
Usage: add num inc?
198+
> java add --inc 10 --num 20
199+
20 + 10 = 30
200+
> java add binary 10 01
201+
Error: invalid argument for num: binary
202+
Error: unused argument: 01
203+
Usage: add num inc?
204+
205+
*/

0 commit comments

Comments
 (0)