ZEAM開発ログ v.0.4.10 マクロを使って micro Elixir のフロントエンドを作ってみる (試行錯誤編)

ZACKY こと山崎進です。

2018年9月28日の fukuoka.ex もくもく会で開発した成果物をお披露目します。

[「ZEAM開発ログ 目次」はこちら](https://qiita.com/zacky1972/items/70593ab2b70d192813df)

# やりたいこと

こんなようなコードがあったとします。

```elixir
def add (a, b) do
  asm do
    add a, b
  end
end
```

これを次のような Elixir のコードにします。

```elixir
def add (a, b) do
  OK.try do
    result <- {:ok, a + b}
  after
    result
  rescue
    :arithmetic_error -> raise ArithmeticError, message: "bad argument in arithmetic expression"
  end
end
```

# なぜこのようなことをしたいのか

[「ZEAM開発ログ v.0.4.7 BigNum をどのようにNIFで扱うか考える」のおまけ](https://qiita.com/zacky1972/items/e2858597de64ec2449dc#%E3%81%8A%E3%81%BE%E3%81%91-max_uint-min_uint-is_uint64-is_bignum-%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F)に現在研究を進めているコード生成のElixir部分が書かれています。

```elixir
  def asm_1(a, b) do
    OK.try do
      result <- case {a, b} do
        {a, b} when is_int64(a)   and is_int64(b)   -> asm_1_nif_ii(a, b)
        {a, b} when is_uint64(a)  and is_uint64(b)  -> asm_1_nif_uu(a, b)
        {a, b} when is_integer(a) and is_integer(b) -> 
          IO.puts "need BigNum"
          {:error, :arithmetic_error}

        ...

        {a, b} when is_float(a)   and is_float(b)   -> asm_1_nif_ff(a, b)
        _ -> {:error, :arithmetic_error}
      end
    after
      result
    rescue
      :arithmetic_error -> raise ArithmeticError, message: "bad argument in arithmetic expression"
    end
  end
```

このようなコードを生成したかったんですね。

# マクロを書いた

[今回の開発成果物も GitHub に置いています。](https://github.com/zeam-vm/asm/tree/elixir-code)

```elixir
  @doc """
  wrap_do_clauses(do_clauses) returns do_clauses if do_clause is a list, otherwise wraps by a list.
	"""
  def wrap_do_clauses do_clauses do
  	if is_list(do_clauses) do
  		do_clauses
  	else
  		[do_clauses]
  	end
  end

  @doc """
  Currently, `asm, do: code` generates wrapper Elixir code of the inline assembler.
  Now, `code` must be `add a, b`.
  """
  defmacro asm clauses do
  	Keyword.get(clauses, :do, nil)
  	|> wrap_do_clauses
  	|> Enum.map(& case elem(&1, 0) do
  			:add -> elem(&1, 2)
  			_ -> raise ArgumentError, "asm supports only add"
  		end)
  	|> Enum.map(& quote do
  			OK.try do
  			  result <- {:ok, unquote({:+, [context: Elixir, import: Kernel], &1})}
  			after
  				result
  			rescue
  				:arithmetic_error -> raise ArithmeticError, message: "bad argument in arithmetic expression"
  			end
  		end)
  end
```

do節を受け取ると，1行の時には単体のタプルが，複数行の時にはタプルのリストが与えられます。そこで，`wrap_do_clauses/1` を使って，どちらもタプルのリストに統一することで，`Enum.map` を使って走査できるようにしました。

`wrap_do_clauses/1` は，引数のパターンマッチを使ったり，ガード節を使ったりすると，もっと洗練した書き方ができますね。

`asm/1` では，最初に`Keyword.get/3`を使ってdo節を受け取り，`wrap_do_clauses/1` でタプルのリストに統一します。

その後，下記のコードで，オペコードに `:add` が来ていたらオペランドを取り出し，そうでなかったら例外を投げます。

```elixir
  	|> Enum.map(& case elem(&1, 0) do
  			:add -> elem(&1, 2)
  			_ -> raise ArgumentError, "asm supports only add"
  		end)
```

その後，下記のコードで，オペランドをもとにコード生成します。

```elixir
  	|> Enum.map(& quote do
  			OK.try do
  			  result <- {:ok, unquote({:+, [context: Elixir, import: Kernel], &1})}
  			after
  				result
  			rescue
  				:arithmetic_error -> raise ArithmeticError, message: "bad argument in arithmetic expression"
  			end
  		end)
```

難しかったのは `a + b` の生成の部分です。オペランドを子ノードにした `:+` 演算子をASTとして生成するコード `unquote({:+, [context: Elixir, import: Kernel], &1})` で実現しました。これは iex を使って下記のように打ち込んだ結果を元に生成しました。

```elixir
iex(1)> quote do: a + b
{:+, [context: Elixir, import: Kernel], [{:a, [], Elixir}, {:b, [], Elixir}]}
iex(2)> 
```


# テストコード

こんな感じのテストコードを書いてみました。

```elixir
defmodule Foo do
  require OK
  require Asm
  import Asm

  def adda(a, b) do
    asm do: add a, b
  end

end

defmodule AsmTest do
  use ExUnit.Case
  doctest Asm
  require Asm

  test "asm do: add a, b" do
    assert Foo.adda(1, 2) == 3
  end
end
```

# 結果

こうなりました。

```bash
$ mix test
warning: this clause cannot match because of different types/sizes
  test/asm_test.exs:18

.

  1) test asm do: add a, b (AsmTest)
     test/asm_test.exs:42
     Assertion with == failed
     code:  assert Foo.adda(1, 2) == 3
     left:  [3]
     right: 3
     stacktrace:
       test/asm_test.exs:43: (test)

...
```

惜しい！ リストを返しています。

# 考察

下記のような感じのインラインアセンブリコード記述が良いかなと思ったのですが，

```elixir
def add (a, b) do
  asm do
    add a, b
  end
end
```

関数ブロックを複数定義できるようにしないといけないので，このままだと難しいなと思うようになりました。そこで，下記のような文法にすることを検討します。

```elixir
def_nif add (a, b) do
  asm add a, b
end
```

このコードをもとに次のようにコード生成します。

```elixir
  def add (a, b) do
    OK.try do
      result <- case {a, b} do
        {a, b} when is_int64(a)   and is_int64(b)   -> asm_add_ii(a, b)
        {a, b} when is_uint64(a)  and is_uint64(b)  -> asm_add_uu(a, b)
        {a, b} when is_integer(a) and is_integer(b) -> 
          IO.puts "need BigNum"
          {:error, :arithmetic_error}

        ...

        {a, b} when is_float(a)   and is_float(b)   -> asm_add_ff(a, b)
        _ -> {:error, :arithmetic_error}
      end
    after
      result
    rescue
      :arithmetic_error -> raise ArithmeticError, message: "bad argument in arithmetic expression"
    end
  end

  def asm_1_nif_ii(a, b) when is_int64(a)  and is_int64(b),  do: raise "NIF asm_1_nif_ii/2 not implemented"
  def asm_1_nif_uu(a, b) when is_uint64(a) and is_uint64(b), do: raise "NIF asm_1_nif_uu/2 not implemented"

  ...
```

というわけで，次回[「ZEAM開発ログ v.0.4.11 マクロを使って micro Elixir のフロントエンドを作ってみる (黎明編)」](https://qiita.com/zacky1972/items/39e7ea9d849e9574a9c3)にて，このような記述の雛形を作ってみたいと思います。