I like your requirements, but it turns out that the proposed 2nd version of the MainAnnotation
already fulfils all of them.
We need to be able to list out all the arguments and all their metadata at once, rather than just fetching values from them one at a time. This is required for useful --help
messages
That functionality is provided by the second version of MainAnnotation
. The annotation can choose to simply collect all getArg
calls and store the meta-information. Then, when it encounters a --help
as actual argument, print out all the stored info.
We want to perform the argument validation applicatively, rather than monadically/imperatively. This is required if we want error messages to be useful and tell us everything we did wrong rather than trickling in one error after another
That’s also possible with the second version of MainAnnotation
. The annotation can choose to keep all validation errors in a buffer that are then printed out together when done
is called. Alternatively, it can record all getArgs
calls as meta-data and validate everything together when done
is called.
We want to be able to support multiple @main
methods! Maybe this is not a hard requirement, but a lot of people really like this feature in Ammonite (as we can see from this thread!) and I make great use of this ability in Mill and Cask.
Since each main function generates its own wrapper class, I don’t see a problem with that. I believe that’s already supported in the current implementation.
We want to be able to return the “remaining” un-parsed arguments to match existing command line conventions. e.g. ssh
you pass in a bunch of flags and then the remaining tokens get treated as a command to run, or python
you pass in a bunch of args, then the remaining tokens get treated as the script name and then script arguments
I don’t see a problem with that either. The main
annotation gets all the actual arguments. So it can do whatever it wants with the arguments that were not requested by the method.
Of course, it’s possible and probably desirable to reify all important info relating to a main method as data, which is what your proposal does. But one does not need to, and I would argue that this reification should not be part of the compiler contract but should be done in a library (maybe the standard library, that would be OK). To give some perspective: I think that even the reliance on Seq
of the compiler is a mistake. A compiled program should not demand anything fancy in terms of interfaces or (even more so) classes. Requiring a MainAnnotation
interface with three methods all taking simply typed arguments is about as fancy as it should get.
My MainAnnotation
proposal is arguably a minimalistic way to describe a main method: The compiler-generated code simply issues calls, one for each argument, that contain the info relevant to this argument. The main
method responds for each argument with a closure that will produce the argument value, if all arguments validate, and that is allowed to fail otherwise. Validation is handled with a simple done
call. Nevertheless, I believe one can implement with this contract a MainAnnotation
that then generates the EntryPoint
, ArgSig
and MainWrapper
classes that you sketched out.
There are two things I am not yet clear about.
First, there’s currently no way in my proposal to handle results of main methods. It’s assumed that the result is Unit
. For Java that looks OK since if one wants an exit value, one can simply call System.exit
. But I am not sure about the general case. Are there important use cases that demand a free choice of return type? What’s the best way to abstract over that? [I guess: Using something like the `ResultHandler` that you had in your earlier proposal].
Second, the current design produces Java main methods in the end so the whole proposal is Java specific. It would probably also work on Native, since the main methods for Java and Unix are basically the same. So is there a need to generalize this further? And, if yes, what’s the simplest way of doing this?
For reference, here’s the latest tweaked MainAnnotation
design:
MainAnnotation
class, defines the contract for the compiler.
trait MainAnnotation extends StaticAnnotation:
type ArgumentParser[T]
// get single argument
def getArg[T](argName: String, fromString: ArgumentParser[T], defaultValue: => Option[T] = None): () => T
// get varargs argument
def getArgs[T](argName: String, fromString: ArgumentParser[T]): () => List[T]
// check that everything is parsed
def done(): Boolean
Sample main
class, can be freely implemented:
class main(progName: String, args: Array[String], docComment: String) extends MainAnnotation
Sample main method
object myProgram:
/** Adds two numbers */
@main def add(num: Int, inc: Int = 1) = println(x + y)
Compiler generated code:
class add:
def main(args: Array[String]) =
val cmd = new main("add", args, "Adds two numbers")
val arg1 = cmd.getArg[Int]("num", summon[cmd.ArgumentParser[Int]])
val arg2 = cmd.getArg[Int]("inc", summon[cmd.ArgumentParser[Int]], Some(1))
if cmd.done() then myProgram.add(arg1(), arg2())