summary refs log tree commit diff stats
path: root/doc/tut3.rst
blob: 720b4fb370989584033ad1a7ef97483e1db1b468 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
=======================
Nim Tutorial (Part III)
=======================

:Author: Arne Döring
:Version: |nimversion|

.. 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*a*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 parseable. 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 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 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 sematic 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
``marcos.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. This means that you can't use backticks within
``quote do:`` for anything else than injecting symbols.  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 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 footstart 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 a
correct argument should be look like.

.. code-block:: nim
    :test: "nim c $1"

  import 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 information 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 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(AssertionError,$`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(AssertionError, $a & " != " & $b)

With Power Comes Responsibility
-------------------------------

Macros are very powerful. A 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 from 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 that 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 though 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>`_