Home

Awesome

Ast Pattern Matching

matchAst(arg, matchErrors):
of nnkStmtList(
  _,
  _,
  nnkForStmt(
    ident"i",
    nnkInfix,
    `mysym` @ nnkStmtList
  )
):
  echo "The AST did match!!!"
  echo "The matched sub tree is the following:"
  echo mysym.lispRepr
else:
  echo matchErrors
  echo "sadly the AST did not match :("

matchAst is where the magic happens. In the of-branch is a tree that is close to the output of lispRepr on an arbitrary NimNode. But it is not quite the same, for example the node kinds still have the nnk-Prefix for the node kinds. The pattern language also has some additional constructs, to widen the possibilities of the pattern.

The grammar for the pattern matching looks like this:

Examples

When you pass a second argument to match as, as in the following example with matchErrors, this identifier will be usable in the else branch as a name for all error kinds. But please use this error type only for debugging purpose. If for the sake of nice error messages the type has to change, it will be changed, please don't rely on the structure of this type.

matchAst(arg, matchErrors):
of <pattern>: # branch A
  discard
of <pattern>: # branch B
  discard
of <pattern>: # branch C
  discard
else:
  echo "branch A could not match because:"
  echo matchErrors[0]
  echo "branch B could not match because:"
  echo matchErrors[1]
  echo "branch C could not match because:"
  echo matchErrors[2]

When you leave out the else branch and there is only one of branch, you will get the nicest error message possible, why the pattern did not match. Just try it out with this example:

let ast = quote do:
  abc
  def
  ghi
  jkl

ast.matchAst:
of nnkStmtList( `a`, `b`, `c`):
  echo "the branch did match"
  echo "but I do know that is impossible"

In this example, you see how recursive pattern matching can be used.

In Recursive pattern matching the ast is traversed recursively. So the patterns are not just matched against arg, but also against all of the children of arg and their children and so on. Recursion stops at nodes that are matched by a pattern. So it might make sense to add some empty of-branches just to cut down the search space. But it does not make sense at all to add an else branch, because then it is just not possible anymore to do recursion at all.

import macros, ast_pattern_matching

macro foobar(arg: untyped): untyped =
  arg.matchAstRecursive:
  of nnkOfBranch( ident"false", `recList` ):
    echo "got false record List: "
    echo recList.treeRepr
  of nnkOfBranch( ident"true", `recList` ):
    echo "got true record List: "
    echo recList.treeRepr


foobar:
  type Obj[T] = object {.inheritable.}
    name: string
    case isFat: bool
    of true:
      m: array[100_000, T]
    of false:
      m: array[10, T]

Do not use break in matchAstRecursive yet, it is not implemented. The name matchAstRecursive is not final and it might get another name or syntax in the future, but this syntax will be kept for backwards compatibility.

for more examples, take a look at the sourcecode. The file tests/test1.nim has a lot of examples that should you get started.

discussion

Maybe at some point the pattern matching library will contain operators to match more flexible patterns with operators such as +, *, |, ?. but that is not implemented. It would be possible though.

It would just raise the question how are named subexpressions are handled in such optional pattern branches.

if the parser allows it to add custom conditions to of branches, such as of <pattern> if a > b: it might replace the of <pattern> |= a > b syntax.

The ast matching statement does not work as an expression (yet).

A matchAstFind that would recursively search though the ast and stop at the first match would make sense. Here it would also be sane to use the else branch for the case that the entire ast does not have any match at all.