Nimでマクロを利用して変数のダンプを行う


# 概要

* デバッグビルド時のみ変数をダンプするNimのマクロを定義して、ヒャッハーします。

## 動機

以前書いた記事[VSCodeでNimのデバッグ(ステップ実行)](https://qiita.com/6in/items/10325115859c93a1d6ba)により、Nimのプログラムコードをステップ実行できるようになったは良いのですが、いざブレークポイントに止まって、ローカル変数を見ようとしても、値が参照できないというという**トホホ**な状況があります。

`args`を見ようとしても、左のペインで内容を確認できない。。。
![Screenshot from 2019-11-12 21-44-09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/87589d46-f918-bae0-4b99-a4892503ae30.png)

## 結局のところ

`echo`を使って変数をコンソールに表示させるようなコードを、ゴニョゴニョ書くことになります。

```nim
echo "args =" & $args
```

とはいえ、デバッグが終われば、先のコードは削除するか、コメントにするみたいな対応をしないといけないです。

## `when defined(debug):`

C/C++のifdefディレクティブのように、nimのコンパイル時に`-d:debug`を指定することにより、ソースコード上でdebugかどうかの判断ができます。

そのため、先のechoのコードを以下のように直すことにより、`-d:debug`でコンパイルした時にのみ`echo`文が動作することになります。

```nim
when defined(debug):
  echo "args =" & $echo
```

ただし、この方法でソースのいろんな箇所に`when ~ echo`のブロックを埋め込んでいくのは、可読性の低下も考慮すると、少々ためらわれます。

## それマクロで

* デバッグビルド時にのみ`echo`で変数のダンプを行うNimのマクロを作成し、シンプルに変数をダンプできるようにします。
* 楽をしたいので、マクロの中で変数名を取得できる機能を使って、`変数名 => 値`という形式で出力してしまいます。

### debugマクロ利用例

```nim
  import debug

  proc sample(x: int, y: string): int =
    result = x + 1

  echo "start"
  let a = 123
  let bbbb = "ABCD"
  let cc = sample(a, bbbb)

  # 変数をダンプ
  debug(
    "parameters", a, bbbb,
    "result", cc)
```

### 結果

```
[debug] ---------------------------
[debug] - - - - - - - - - - - - - -
[debug] parameters
[debug] - - - - - - - - - - - - - -
[debug]    a => 123
[debug] bbbb => ABCD
[debug] - - - - - - - - - - - - - -
[debug] result
[debug] - - - - - - - - - - - - - -
[debug]   cc => 124
[debug] ---------------------------
```

## マクロの仕様

* `debug`に渡せるのは、文字列リテラルか変数のみで複数の指定が可能。
  * 関数呼び出し等のステートメントを渡すことはできません。
  * `debug func(1,2)` とかはダメ。 
* 変数を渡した場合、`変数名 => 値`の形式で出力
* 変数名は**右揃え**で表示
* 文字列リテラルを渡した場合には、その**文字列のみを出力**
* ダンプできる変数の型が**`$`関数**を実装していること
* `/dev/tty`が利用できるならそちらへ出力するため、**ダンプした内容はリダイレクトされません**


## マクロはこちら

```nim:debug.nim
import macros
import strformat
import strutils

macro debug*(n: varargs[untyped]): untyped =
  var buff = fmt"""
when defined(debug):
  block:
    var useTty = false
    let tty = 
      try:
        let term = open("/dev/tty", fmWrite)
        useTty = true          
        term
      except IOError:
        stdout
    defer:
      if useTty:
        tty.close
    tty.writeLine ("[debug] ---------------------------")
"""
  # get max variable length
  var maxLen = 0
  for x in n:
    if x.kind == nnkIdent:
      let l = ($x).len
      if l > maxLen:
        maxLen = l

  # dump variable
  for x in n:
    # echo x.kind
    if x.kind == nnkStrLit:
      buff = buff & fmt"""
    tty.writeLine ("[debug] - - - - - - - - - - - - - -")
    tty.writeLine ("[debug] " & {repr(x)})
    tty.writeLine ("[debug] - - - - - - - - - - - - - -")
"""
    if x.kind == nnkIdent:
      let name = " ".repeat(maxLen - ($x).len) & $x
      buff = buff & fmt"""
    tty.writeLine ("[debug] {name} => " & ${repr(x)})
  # tty.writeLine ("[debug] ---------------------------")
"""
  buff = buff & """
    tty.writeLine ("[debug] ---------------------------")
"""
  # echo buff
  parseStmt(buff)

macro if_debug*(head: untyped, body: untyped): untyped =
  let bodies = repr(body).replace("\x0a", "\x0a    ")
  let stmt = fmt"""
when defined(debug): 
  block:
    let stage = {repr(head)}
    debug stage{bodies}
"""
  # echo stmt
  result = parseStmt(stmt)

when isMainModule:
  proc sample(x: int, y: string): int =
    result = x + 1

  echo "start"
  let a = 123
  let bbbb = "ヒャッハー"
  let cc = sample(a, bbbb)
  debug(
    "parameters", a, bbbb,
    "result", cc)

  # debug block
  if_debug("step 1"):
    for x in 1..3:
      debug("loop", x)

  if_debug("step 2"):
    debug "this is debug"

  if_debug("step 3"):
    debug "this is debug"
```

## 利用方法

* `debug.nim`を開発中のソースがあるフォルダに作成し上記のソースをコピペ
* 利用したいソースで、`import debug`を追加する
* `debug` メソッドに、ローカル/グローバル変数を渡す
* nimのコンパイラで、`-d:debug`を指定してコンパイル＆実行

```nim:sample.nim
import debug

let a = 1
let b = "ヒャッハー"

debug a, b
```

```shell
# コンパイル＆実行
nim c -r -d:debug sample.nim
```

```shell-session
[debug] ---------------------------
[debug] a => 123
[debug] b => ヒャッハー
[debug] ---------------------------
```

* `debug a, b`は以下のように展開されています。

```nim
when defined(debug):
  block:
    var useTty = false
    let tty = 
      try:
        let term = open("/dev/tty", fmWrite)
        useTty = true          
        term
      except IOError:
        stdout
    defer:
      if useTty:
        tty.close
    tty.writeLine ("[debug] ---------------------------")
    tty.writeLine ("[debug] a => " & $a)
    tty.writeLine ("[debug] b => " & $b)
    tty.writeLine ("[debug] ---------------------------")
```

* `/dev/tty`を使うアイデアは、@jiro4989さんの[端末のパイプ先に特定の出力だけ渡す方法](https://qiita.com/jiro4989/items/c9bf9c72174692d2e551)を参考にさせていただきました。
* ttyとstdoutの切り替えを行っているのは、`gdb`を利用したデバッグを行っている時に`tty`が使えない制限があったためです。

## マクロコワクナイヨ

* 今回作成したマクロは、展開したいNimのコードを組み立てた文字列をparseStmtに渡すことにより、マクロの展開を行っています。
* Nimとして展開したいコードを生成すればよいので、この方法だと容易にマクロが定義できるんだなと思いました。
* マクロ自体のデバッグは、マクロ内で`echo`することで、マクロの実行における動作確認ができます。
* マクロを作成するのにあたり、@gmShibaさんの[実践Nimマクロ](https://qiita.com/gmShiba/items/b06fff4c4ef98769c781)を参考にさせてもらいました。

## まとめ

* `debug`マクロを使うことにより、変数の内容の確認が気軽にできるので、開発時の御供にしていただければ幸いです。
* 今回のマクロは拙作の[nimのパッケージマネージャnimbleによるプロジェクトテンプレート](https://qiita.com/6in/items/81dd2f7fdf292149874b)にも組み込んでいます。