I'm trying to make an argument parser as an excuse to learn how to use macros and templates. But I'm getting stuck.
As a starting point, my goal is to use macros and templates to convert this:
var p = mkParser("Some Program"):
flag("-a")
flag("-b", "--optionb", help = "This is an option")
into this:
var p =
type
Parser[T] = object
helptext*: string
Opts = object
a*: bool
optionb*: bool
var tmp = Parser[Opts]()
tmp.helptext = """Some Program
-a
--optionb,-b This is an option
"""
tmp
(I'll eventually want to add code to actually parse arguments, but being able to do this first step is currently a hurdle for me).
The puzzle is how to implement these two things:
Also, I want code-completion with the flag procedure.
I wish I had saved each of my non-working attempts, but I only have my memory, unfortunately.
single macro
I first attempted to define macro mkParser which reads through it's child nodes, looks for calls named flag and interprets the arguments -- all within the body of the macro. While this worked (at least as far as I pursued it), it suffers from not having a flag proc/macro/template defined anywhere. That is, if I generate the documentation, there's no flag proc/macro/template.
This approach seems unsustainable.
template + macro
I also attempted with template mkParser and macro flag. This helped solidify for me that templates are really just substitutions and the code within them represent pretty much what the code will look like at runtime.
The main sticking point with my problem is that I want mkParser to iterate through each of it's children and evaluate them to add information to a list of flags that I'm building up.
In pseudo code:
macro mkParser(body) =
var listofFlags = @[]
for child in body:
let data = child()
listofFlags.add(data.name)
result = listofFlag.makeSomeTypeDefinitions()
macro + macro
I also attempted with macro mkParser and macro flag but it had essentially the same problem as the template + macro attempt.
I've read and re-read how the unittest module works, though it uses some deprecated features so I'm not sure it's the best model.
If I could figure out how to run through the children of a macro/template to build up some data structures then I feel like I could build the AST pretty easily (either by hand or with quote using something like this):
type
Flag = object
name*: string
kind*: string
macro mkOptType(flags:seq[Flag]): untyped =
var flagdefs:string
for flag in flags:
flagdefs.add(&"{flag.name}*: {flag.kind}\n ")
result = quote do:
type
Parser[T] = object
helptext*: string
type
Opts = object
`flagdefs`
var p = Parser[Opts]()
p
)
Btw, you can be even more ambigious with your dsl:
mkParser("Some Program"):
-a
-b --optionb "This is an option"
You can also have this.
Now, writing a macro for this might be a little bit complicated, but it's a good example that macros can be more expressive
I think the first approach is fine: flag is part of your dsl, so I am not sure what would a proc/macro flag would do outside of it (e.g. what will help do?)
You should document your dsl in the mkParser/module docs anyway
I think I've found a solution that will work for me (still working through all the details):
I define mkParser and flag as {.compileTime.} procs. Then when I want to make a command line parser, in my application code, I define and use a macro like this:
macro makeParser(): untyped =
return mkParser("some name"):
flag("-a")
flag("-b")
var p = makeParser()
I think it would be cool if Nim supported doing stuff like this without the wrapping macro syntax, but it's way cool that Nim supports this at all!
Nim does support your original example, e.g.
macro mkParser*(name: untyped, args: untyped): untyped =
# process name and args here
# generate the setup code(type & parser) with possibly
# compile time proc helpers
# generate the flag code directly here
# or with flag(..) which will be invoked later
discard
# if you demand autocompletion, this is a hack, but it's a very simple solution: and it describes correctly the flag function in your dsl
proc flag*(names: varargs[string], help: string = "") =
discard
If you demand flag to be explicitly defined, use a macro, not a compile time function: this way you can
block:
assign tmpParser
flag(..)
and generate code working on tmpParser in flag . This way you'll have your macro mkParser and your macro flag with the right signature and autocompletion