123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- =======================
- Nim Tutorial (Part III)
- =======================
- :Author: Arne Döring
- :Version: |nimversion|
- .. default-role:: code
- .. include:: rstcommon.rst
- .. contents::
- Introduction
- ============
- "With Great Power Comes Great Responsibility." -- Spider Man's Uncle
- This document is a tutorial about Nim's macro system.
- A macro is a function that is executed at compile-time and transforms
- a Nim syntax tree into a different tree.
- Examples of things that can be implemented in macros:
- * An assert macro that prints both sides of a comparison operator, if
- the assertion fails. `myAssert(a == b)` is converted to
- `if a != b: quit($a " != " $b)`
- * A debug macro that prints the value and the name of the symbol.
- `myDebugEcho(a)` is converted to `echo "a: ", a`
- * Symbolic differentiation of an expression.
- `diff(a*pow(x,3) + b*pow(x,2) + c*x + d, x)` is converted to
- `3*a*pow(x,2) + 2*b*x + c`
- Macro Arguments
- ---------------
- The types of macro arguments have two faces. One face is used for
- the overload resolution and the other face is used within the macro
- body. For example, if `macro foo(arg: int)` is called in an
- expression `foo(x)`, `x` has to be of a type compatible to int, but
- *within* the macro's body `arg` has the type `NimNode`, not `int`!
- Why it is done this way will become obvious later, when we have seen
- concrete examples.
- There are two ways to pass arguments to a macro, an argument can be
- either `typed` or `untyped`.
- Untyped Arguments
- -----------------
- Untyped macro arguments are passed to the macro before they are
- semantically checked. This means the syntax tree that is passed down
- to the macro does not need to make sense for Nim yet, the only
- limitation is that it needs to be parsable. Usually, the macro does
- not check the argument either but uses it in the transformation's
- result somehow. The result of a macro expansion is always checked
- by the compiler, so apart from weird error messages, nothing bad
- can happen.
- The downside for an `untyped` argument is that these do not play
- well with Nim's overloading resolution.
- The upside for untyped arguments is that the syntax tree is
- quite predictable and less complex compared to its `typed`
- counterpart.
- Typed Arguments
- ---------------
- For typed arguments, the semantic checker runs on the argument and
- does transformations on it, before it is passed to the macro. Here
- identifier nodes are resolved as symbols, implicit type
- conversions are visible in the tree as calls, templates are
- expanded, and probably most importantly, nodes have type information.
- Typed arguments can have the type `typed` in the arguments list.
- But all other types, such as `int`, `float` or `MyObjectType`
- are typed arguments as well, and they are passed to the macro as a
- syntax tree.
- Static Arguments
- ----------------
- Static arguments are a way to pass values as values and not as syntax
- tree nodes to a macro. For example for `macro foo(arg: static[int])`
- in the expression `foo(x)`, `x` needs to be an integer constant,
- but in the macro body `arg` is just like a normal parameter of type
- `int`.
- .. code-block:: nim
- import std/macros
- macro myMacro(arg: static[int]): untyped =
- echo arg # just an int (7), not `NimNode`
- myMacro(1 + 2 * 3)
- Code Blocks as Arguments
- ------------------------
- It is possible to pass the last argument of a call expression in a
- separate code block with indentation. For example, the following code
- example is a valid (but not a recommended) way to call `echo`:
- .. code-block:: nim
- echo "Hello ":
- let a = "Wor"
- let b = "ld!"
- a & b
- For macros this way of calling is very useful; syntax trees of arbitrary
- complexity can be passed to macros with this notation.
- The Syntax Tree
- ---------------
- In order to build a Nim syntax tree one needs to know how Nim source
- code is represented as a syntax tree, and how such a tree needs to
- look like so that the Nim compiler will understand it. The nodes of the
- Nim syntax tree are documented in the `macros <macros.html>`_ module.
- But a more interactive way to explore the Nim
- syntax tree is with `macros.treeRepr`, it converts a syntax tree
- into a multi-line string for printing on the console. It can be used
- to explore how the argument expressions are represented in tree form
- and for debug printing of generated syntax tree. `dumpTree` is a
- predefined macro that just prints its argument in a tree representation,
- but does nothing else. Here is an example of such a tree representation:
- .. code-block:: nim
- dumpTree:
- var mt: MyType = MyType(a:123.456, b:"abcdef")
- # output:
- # StmtList
- # VarSection
- # IdentDefs
- # Ident "mt"
- # Ident "MyType"
- # ObjConstr
- # Ident "MyType"
- # ExprColonExpr
- # Ident "a"
- # FloatLit 123.456
- # ExprColonExpr
- # Ident "b"
- # StrLit "abcdef"
- Custom Semantic Checking
- ------------------------
- The first thing that a macro should do with its arguments is to check
- if the argument is in the correct form. Not every type of wrong input
- needs to be caught here, but anything that could cause a crash during
- macro evaluation should be caught and create a nice error message.
- `macros.expectKind` and `macros.expectLen` are a good start. If
- the checks need to be more complex, arbitrary error messages can
- be created with the `macros.error` proc.
- .. code-block:: nim
- macro myAssert(arg: untyped): untyped =
- arg.expectKind nnkInfix
- Generating Code
- ---------------
- There are two ways to generate the code. Either by creating the syntax
- tree with expressions that contain a lot of calls to `newTree` and
- `newLit`, or with `quote do:` expressions. The first option offers
- the best low-level control for the syntax tree generation, but the
- second option is much less verbose. If you choose to create the syntax
- tree with calls to `newTree` and `newLit` the macro
- `macros.dumpAstGen` can help you with the verbosity.
- `quote do:` allows you to write the code that you want to generate literally.
- Backticks are used to insert code from `NimNode` symbols into the
- generated expression.
- .. code-block:: nim
- macro a(i) = quote do: let `i` = 0
- a b
- A custom prefix operator can be defined whenever backticks are needed.
- .. code-block:: nim
- macro a(i) = quote("@") do: assert @i == 0
- let b = 0
- a b
- The injected symbol needs accent quoted when it resolves to a symbol.
- .. code-block:: nim
- macro a(i) = quote("@") do: let `@i` == 0
- a b
- Make sure to inject only symbols of type `NimNode` into the generated syntax
- tree. You can use `newLit` to convert arbitrary values into
- expressions trees of type `NimNode` so that it is safe to inject
- them into the tree.
- .. code-block:: nim
- :test: "nim c $1"
- import std/macros
- type
- MyType = object
- a: float
- b: string
- macro myMacro(arg: untyped): untyped =
- var mt: MyType = MyType(a:123.456, b:"abcdef")
- # ...
- let mtLit = newLit(mt)
- result = quote do:
- echo `arg`
- echo `mtLit`
- myMacro("Hallo")
- The call to `myMacro` will generate the following code:
- .. code-block:: nim
- echo "Hallo"
- echo MyType(a: 123.456'f64, b: "abcdef")
- Building Your First Macro
- -------------------------
- To give a starting point to writing macros we will show now how to
- implement the `myDebug` macro mentioned earlier. The first thing to
- do is to build a simple example of the macro usage, and then just
- print the argument. This way it is possible to get an idea of what a
- correct argument should look like.
- .. code-block:: nim
- :test: "nim c $1"
- import std/macros
- macro myAssert(arg: untyped): untyped =
- echo arg.treeRepr
- let a = 1
- let b = 2
- myAssert(a != b)
- .. code-block::
- Infix
- Ident "!="
- Ident "a"
- Ident "b"
- From the output, it is possible to see that the argument is an infix
- operator (node kind is "Infix"), as well as that the two operands are
- at index 1 and 2. With this information, the actual macro can be
- written.
- .. code-block:: nim
- :test: "nim c $1"
- import std/macros
- macro myAssert(arg: untyped): untyped =
- # all node kind identifiers are prefixed with "nnk"
- arg.expectKind nnkInfix
- arg.expectLen 3
- # operator as string literal
- let op = newLit(" " & arg[0].repr & " ")
- let lhs = arg[1]
- let rhs = arg[2]
- result = quote do:
- if not `arg`:
- raise newException(AssertionDefect,$`lhs` & `op` & $`rhs`)
- let a = 1
- let b = 2
- myAssert(a != b)
- myAssert(a == b)
- This is the code that will be generated. To debug what the macro
- actually generated, the statement `echo result.repr` can be used, in
- the last line of the macro. It is also the statement that has been
- used to get this output.
- .. code-block:: nim
- if not (a != b):
- raise newException(AssertionDefect, $a & " != " & $b)
- With Power Comes Responsibility
- -------------------------------
- Macros are very powerful. A piece of good advice is to use them as little as
- possible, but as much as necessary. Macros can change the semantics of
- expressions, making the code incomprehensible for anybody who does not
- know exactly what the macro does with it. So whenever a macro is not
- necessary and the same logic can be implemented using templates or
- generics, it is probably better not to use a macro. And when a macro
- is used for something, the macro should better have a well-written
- documentation. For all the people who claim to write only perfectly
- self-explanatory code: when it comes to macros, the implementation is
- not enough for documentation.
- Limitations
- -----------
- Since macros are evaluated in the compiler in the NimVM, macros share
- all the limitations of the NimVM. They have to be implemented in pure Nim
- code. Macros can start external processes on the shell, but they
- cannot call C functions except those that are built in the
- compiler.
- More Examples
- =============
- This tutorial can only cover the basics of the macro system. There are
- macros out there that could be an inspiration for you of what is
- possible with it.
- Strformat
- ---------
- In the Nim standard library, the `strformat` library provides a
- macro that parses a string literal at compile time. Parsing a string
- in a macro like here is generally not recommended. The parsed AST
- cannot have type information, and parsing implemented on the VM is
- generally not very fast. Working on AST nodes is almost always the
- recommended way. But still `strformat` is a good example for a
- practical use case for a macro that is slightly more complex than the
- `assert` macro.
- `Strformat <https://github.com/nim-lang/Nim/blob/5845716df8c96157a047c2bd6bcdd795a7a2b9b1/lib/pure/strformat.nim#L280>`_
- Ast Pattern Matching
- --------------------
- Ast Pattern Matching is a macro library to aid in writing complex
- macros. This can be seen as a good example of how to repurpose the
- Nim syntax tree with new semantics.
- `Ast Pattern Matching <https://github.com/krux02/ast-pattern-matching>`_
- OpenGL Sandbox
- --------------
- This project has a working Nim to GLSL compiler written entirely in
- macros. It scans recursively through all used function symbols to
- compile them so that cross library functions can be executed on the GPU.
- `OpenGL Sandbox <https://github.com/krux02/opengl-sandbox>`_
|