Passing complex parameters to a Nimrod macro

353 views Asked by At

I would like to pass configuration parameters to a macro. I already have a procedure that produce a string of Nimrod code based on these parameters (a tuple of sequences). I know that I can pass a string and convert it with strVal (as shown the answer https://stackoverflow.com/a/19956317/334703). Can I do the same with more complex data?

Alternatively can I use this string of Nimrod code in a compile-time procedure with a call to a procedure such as parseStmt?

EDIT: The generation of Nimrod code was useful to test my ideas bit I agree I should probably generate AST directly.

Here is an example of the structure I'm thinking about.

type
  Tconfig = tuple
    letters: seq[string]
    numbers:seq[int]

var
  data = (@("aa", "bb"), @(11, 22))

macro mymacro(data: Tconfig): stmt =
   ...
3

There are 3 answers

1
Grzegorz Adam Hankiewicz On BEST ANSWER

If you need or want to traverse the structure of the data in the macro, first you need to make the variable a const. var are for runtime, so the macro will just get a nnkSym node. Once you make that a const you get the same input as if you had typed yourself the value manually there. I'll use the treeRepr macro and plenty of echo to show you what kind of AST you get and how you would walk it:

import macros

type
  Tconfig = tuple
    letters: seq[string]
    numbers:seq[int]

const data: Tconfig = (@["aa", "bb"], @[11, 22])

macro mymacro(data: Tconfig): stmt =
  echo "AST being passed in:\n", treeRepr(data)
  echo "root type is ", data.kind
  echo "number of children ", len(data)
  let n1 = data[0]
  echo "first child is ", n1.kind
  echo "first child children ", len(n1)
  let e2 = n1[1]
  echo "second exp child is ", e2.kind
  echo "second exp child children ", len(e2)
  let v1 = e2[0]
  echo "first seq value is ", v1.kind
  echo "first seq value children ", len(v1)
  echo "Final literal is ", v1.strVal

when isMainModule:
  mymacro(data)

When I compile that example I get the following output:

AST being passed in:
Par
  ExprColonExpr
    Sym "letters"
    Bracket
      StrLit aa
      StrLit bb
  ExprColonExpr
    Sym "numbers"
    Bracket
      IntLit 11
      IntLit 22
root type is nnkPar
number of children 2
first child is nnkExprColonExpr
first child children 2
second exp child is nnkBracket
second exp child children 2
first seq value is nnkStrLit
first seq value children 0
Final literal is aa
1
Reimer Behrends On

I'm not 100% certain what you mean, but from the context, it looks as though the functionality you need is that of macros.toStrLit, which generates a string literal node from an AST. Example:

import macros, strutils

macro showExpr(x: expr): stmt =
  parseStmt("echo(" & x.toStrLit.strVal.escape & ")")

showExpr("x" & "y")

Consider transforming the AST directly, though, because generating and reparsing code as strings can lead to surprises with quoting (note the .escape above), indentation, etc.

0
zah On

if your macro needs to process the actual constant data passed to it, the recommended way is to use static params:

type
  TConfig = tuple
    letters: seq[string]
    numbers:seq[int]

const data = (@["aa", "bb"], @[11, 22])

macro mymacro(cfg: static[TConfig]): stmt =
  echo "letters"
  for s in cfg.letters:
    echo s

  echo "numbers"
  for n in cfg.numbers:
    echo n

mymacro(data)

This approach have multiple benefits:

1) Instead of getting a raw AST, the input param cfg will have the TConfig type inside the macro body, so you can more easily access its members as shown in the example.

2) The compiler will automatically evaluate complex expressions that produce TConfig values, when they are used with the macro (e.g. mymacro(mergeConfigs(userConfig, systemConfig)), assuming that mergeConfigs is some proc that could be evaluated at compile-time).

For more information regarding static params, please refer to the manual:
http://nim-lang.org/manual.html#static-t