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
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.
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
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 class, defines the contract for the compiler.
trait MainAnnotation extends StaticAnnotation:
// 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
main class, can be freely implemented:
class main(progName: String, args: Array[String], docComment: String) extends MainAnnotation
Sample main method
/** Adds two numbers */
@main def add(num: Int, inc: Int = 1) = println(x + y)
Compiler generated code:
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())