Hacker News new | past | comments | ask | show | jobs | submit login

Metaprogramming is one of the core ideals in the birth of the language so is well supported. Personally I've not used Lisp, so can't comment there, but it is on the list of influences on the homepage.

Essentially there's a VM that runs almost all the language barring importc type stuff, and you can chuck around AST node objects to create code, so metaprogramming is done in the core Nim language. You can read files so it's easy to slurp files and use them to generate code or other data processing at compile-time.

Several simple utility operations in the stdlib make things really fluid; the easy ability to `quote` blocks of code to AST, and outputting nodes and code to string. This lets you both hack something together quickly and learn over time how the syntax trees work.

Quoting looks like this:

  macro repeat(count: int, code: untyped): untyped =
    quote do:
      for i in 0..<`count`:
        `code`

  repeat(10):
    echo "Hello"
Inspecting something's AST can be done with dumpTree:

  dumpTree:
    let
      x = 1
      y = 2
    echo "Hello ", x + y
To save even more effort, there's also dumpAstGen which outputs the code to create that node manually, and even a dumpLisp!

You can display ASTs inside macros:

  macro showMe(input: untyped): untyped =
    echo "Input as written:", input.repr
    echo "Input as AST tree:", input.treerepr
    result = quote: `input` + `input`
    echo result.repr
    echo result.treerepr
So it's really easy to debug what went wrong if you're generating lots of code.

Since untyped parameters to a macro don't have to be valid Nim code (though they still follow syntax rules) you can make your own DSLs really easily and reliably because any input is pre-parsed into a nice tree for you.

Here's a contrived example of some simple DSL that lets you call procs and store their results for later output:

  import macros, tables
  
  macro process(items: untyped): untyped =
    result = newStmtList()
    # Create hash table of 'perform' names to store their result variables.
    var performers: Table[string, NimNode]
  
    for item in items:
      let
        command = item[0]
        param = item[1]
        paramStr = $param
  
      case $command
      of "perform":
        # Check if we've already generated a var for holding the return value.
        var node = performers.getOrDefault(paramStr)
        if node == nil:
          # Generate a variable name to store the performer result in.
          # genSym guarantees a unique name.
          node = genSym(nskVar, paramStr)
          performers.add(paramStr, node)

          # Add the variable declaration
          result.add(quote do:
            var `node` = `param`()
          )
        else:
          # A repeat performance, we don't need to declare the variable and can overwrite the
          # value in the fetched variable.
          result.add(quote do:
            `node` = `param`()
            )
      of "output":
        let node = performers.getOrDefault(paramStr)
        if node == nil: quit "Cannot find performer " & paramStr
        result.add(quote do:
          echo `node`)
      else: discard
    # Display the resultant code.
    echo result.repr

  proc foo: string = "foo!"
  proc bar: string = "bar!"

  process:
    perform foo
    perform bar
    perform foo
    output foo
    output bar
The generated output from process looks like:

  var foo262819 = foo()
  var bar262821 = bar()
  foo262819 = foo()
  echo foo262819
  echo bar262821



In hindsight maybe the "performers" table would be better named "resultVars", as that's what it really represents.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: