ZEAM開発ログ v.0.4.11 マクロを使って micro Elixir のフロントエンドを作ってみる (黎明編)

ZACKYこと山崎進です。

fukuoka.ex もくもく会が終わって帰宅後，なんだか寝付けずに未明までプログラミングを続けていて，けっこうな成果になったので報告します。

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

**追記: context の問題があったのでデバッグしました**

# 実現できたこと

[成果物はこちら (GitHub)](https://github.com/zeam-vm/asm/tree/def_nif)

こういうコードを書くと

```elixir
defmodule Foo do
  require Asm
  import Asm

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

仮にこんな感じに展開してくれるマクロを作りました。

```elixir
defmodule Foo do
  require Asm
  import Asm

  def add(a, b), do: a + b

  def add_ii(a, b) when is_int64(a) and is_int64(b), do: a + b
  def add_uu(a, b) when is_uint64(a) and is_uint64(b), do: a + b
  def add_ff(a, b) when is_float(a) and is_float(b), do: a + b
end
```

# 書いたマクロ

lib/asm.ex

```elixir
defmodule Asm do
  use Constants

  @name :max_int
  @value 0x7fff_ffff_ffff_ffff

  @name :min_int
  @value -0x8000_0000_0000_0000


  @name :max_uint
  @value 0xffff_ffff_ffff_ffff

  @name :min_uint
  @value 0



  @moduledoc """
  Asm aimed at implementing an inline assembler.

  Currently, it provides the following:

  * `is_int64` macro that can be used in `when` clauses to judge that a value is within INT64.
  * `is_uint64` macro that can be used in `when` clauses to judge that a value is within UINT64.
  * `is_bignum` macro that can be used in `when` clauses to judge that a value needs BigNum representation, that is, it is an integer but not within INT64 nor UINT64.
  * `max_int` is the constant value of maxium of INT64.
  * `min_int` is the constant value of minimum of INT64.
  """

  @doc """
  is_int64(value) returns true if the value is a signed integer, equals or is less than max_int and equals or is greater than min_int.
  """
  defmacro is_int64(value) do
    quote do
    	is_integer(unquote(value))
    	and unquote(value) <= unquote(Asm.max_int)
    	and unquote(value) >= unquote(Asm.min_int)
    end
  end

  @doc """
  is_uint64(value) returns true if the value is an unsigned integer, equals or is less than max_uint and equals or is greater than min_uint.
  """
  defmacro is_uint64(value) do
    quote do
      is_integer(unquote(value))
      and unquote(value) <= unquote(Asm.max_uint)
      and unquote(value) >= unquote(Asm.min_uint)
    end
  end

  @doc """
  is_bignum(value) returns true if the value is an integer but larger than max_uint and smaller than min_int.
  """
  defmacro is_bignum(value) do
    quote do
      is_integer(unquote(value))
      and (unquote(value) > unquote(Asm.max_uint)
      or unquote(value) < unquote(Asm.min_int))
    end
  end

  @doc """
  make_clauses makes a clause or clauses into a list of it / them.

  ## Examples
    iex> Asm.make_clauses({:tuple})
    [{:tuple}]

    iex> Asm.make_clauses([{:a}, {:b}])
    [{:a}, {:b}]
  """
  def make_clauses(clause) when is_tuple(clause), do: [clause]
  def make_clauses(clauses) when is_list(clauses), do: clauses

  @doc """
  unwrap_do eliminates the do header from do clauses and makes them into a list of clauses

  ## Examples
    iex> Asm.wrap_do(quote do: 1 + 2) |> Asm.unwrap_do
    [{:+, [context: AsmTest, import: Kernel], [1, 2]}]
  """
  def unwrap_do(do_clauses) do
  	do_clauses
  	|> Keyword.get(:do, nil)
  	|> make_clauses
  end

  @doc """
  wrap_do generates do clauses wrapping the orginal clauses with :do

  ## Examples
    iex> Asm.wrap_do(quote do: 1 + 2)
    [do: {:+, [context: AsmTest, import: Kernel], [1, 2]}]
  """
  def wrap_do(clauses) do
  	Keyword.put([], :do, clauses)
  end

  @doc """
  get_name(func) gets the name of the function.

  ## Examples
    iex> Asm.get_name(quote do: func(a, b))
    "func"
  """
  def get_name(func) do
  	elem(func, 0)
  	|> Atom.to_string
  end

  @doc """
  args(func) gets the arguments of the function.

  ## Examples
    iex> Asm.args(quote do: func(a, b))
    [{:a, [], AsmTest}, {:b, [], AsmTest}]
  """
  def args(func) do
  	elem(func, 2)
  end

  @doc """
  arity(func) gets the arity of the function.

  ## Examples
    iex> Asm.arity(quote do: func(a, b))
    2
  """
  def arity(func) do
  	func |> args |> length
  end

  @doc """
  get_name_arity(func) gets the name with the arity of the function.

  ## Examples
    iex> Asm.get_name_arity(quote do: func(a, b))
    "func/2"
  """
  def get_name_arity(func) do
    get_name(func) <> "/" <> Integer.to_string(arity(func))
  end


  @doc """
  get_name_all(type, func) generates a variation of a name of the function :func_ii that has the type like "i".

  ## Examples
    iex> Asm.get_name_all("i", quote do: func(a, b))
    :func_ii
  """
  def get_name_all(type, func) do
  	(get_name(func) <> "_" <> (1..arity(func) |> Enum.map(fn _ -> type end) |> Enum.join()))
  	|> String.to_atom
  end

  @doc """
  get_func_all(type, func) generates a variation of the function :func_ii that has the type like "i", the location of line and the arguments same to the original function.

  ## Examples
    iex> Asm.get_func_all("i", quote do: func(a, b))
    {:func_ii, [], [{:a, [], AsmTest}, {:b, [], AsmTest}]}
  """
  def get_func_all(type, func) do
  	{get_name_all(type, func), elem(func, 1), elem(func, 2)}
  end

  @doc """
  when_and_int64(func, module) generates the function in the context of the module with a when clause that all of arguments of the function should be int64.

  ## Examples
  	iex> Asm.when_and_int64(quote do func(a,b) end, nil) |> Macro.to_string
  	"func_ii(a, b) when is_int64(a) and is_int64(b)"
  """
  def when_and_int64(func, nil), do: when_and_int64(func, Elixir)
  def when_and_int64(func, module) do
  	quote do
  		unquote(get_func_all("i", func))
  		when unquote({:and, [context: module, import: Kernel],
  			args(func)
  			|> Enum.map(& quote do: is_int64(unquote(&1)))
  		})
  	end
  end

  @doc """
  when_and_uint64(func, module) generates the function in the context of the module with a when clause that all of arguments of the function should be uint64.

  ## Examples
  	iex> Asm.when_and_uint64(quote do func(a,b) end, nil) |> Macro.to_string
  	"func_uu(a, b) when is_uint64(a) and is_uint64(b)"
  """
  def when_and_uint64(func, nil), do: when_and_uint64(func, Elixir)
  def when_and_uint64(func, module) do
  	quote do
  		unquote(get_func_all("u", func))
  		when unquote({:and, [context: module, import: Kernel],
  			args(func)
  			|> Enum.map(& quote do: is_uint64(unquote(&1)))
  		})
  	end
  end

  @doc """
  when_and_float(func, module) generates the function with a when clause that all of arguments of the function should be float.

  ## Examples
  	iex> Asm.when_and_float(quote do func(a,b) end, nil) |> Macro.to_string
  	"func_ff(a, b) when is_float(a) and is_float(b)"
  """
  def when_and_float(func, nil), do: when_and_float(func, Elixir)
  def when_and_float(func, module) do
  	quote do
  		unquote(get_func_all("f", func))
  		when unquote({:and, [context: module, import: Kernel],
  			args(func)
  			|> Enum.map(& quote do: is_float(unquote(&1)))
  		})
  	end
  end

  @doc """
  asm generates a fragment of assembly code.
  """
  defmacro asm clause do
 		operands = case elem(clause, 0) do
 			:add -> elem(clause, 2)
 			_ -> raise ArgumentError, "asm supports only add"
 		end
 		quote do
 			 unquote({:+, [context: Elixir, import: Kernel], operands})
 		end
  end

  @doc """
  def_nif defines a NIF that includes micro Elixir code.
  """
  defmacro def_nif func, do_clause do
  	quote do
  		def unquote(func), unquote(do_clause)
  		def unquote(when_and_int64(func, __ENV__.module)), unquote(do_clause)
  		def unquote(when_and_uint64(func, __ENV__.module)), unquote(do_clause)
  		def unquote(when_and_float(func, __ENV__.module)), unquote(do_clause)
  	end
  end

end
```

# 解説

では順を追って説明します。

## def_nif

```elixir
  @doc """
  def_nif defines a NIF that includes micro Elixir code.
  """
  defmacro def_nif func, do_clause do
  	quote do
  		def unquote(func), unquote(do_clause)
  		def unquote(when_and_int64(func, __ENV__.module)), unquote(do_clause)
  		def unquote(when_and_uint64(func, __ENV__.module)), unquote(do_clause)
  		def unquote(when_and_float(func, __ENV__.module)), unquote(do_clause)
  	end
  end
```

`def_nif` の本体です。

例題 `def_nif add(a, b), do: asm add a, b` の場合について説明します。

* `def unquote(func), unquote(do_clause)` は，`def add(a, b), do: a + b` を生成します。`unquote(do_clause)` の中で `asm add a, b` はマクロ `asm` によって仮に `a + b` に変換されます。実際には Elixir 側の型多相かつ型安全な NIF の呼び出しのコードに置き換える予定です。
* `def unquote(when_and_int64(func, __ENV__.module), unquote(do_clause)` は，`def asm_ii(a, b), do: a + b` を生成します。実際には NIF 呼び出しのコードに置き換える予定です。__ENV__.module は現在のモジュール名の取得で，`when_and_int64` にコンテキストを渡します。
* `def unquote(when_and_uint64(func, __ENV__.module), unquote(do_clause)` は，`def asm_uu(a, b), do: a + b` を生成します。
* `def unquote(when_and_float(func, __ENV__.module), unquote(do_clause)` は，`def asm_ff(a, b), do: a + b` を生成します。

## asm

```elixir
  @doc """
  asm generates a fragment of assembly code.
  """
  defmacro asm clause do
 		operands = case elem(clause, 0) do
 			:add -> elem(clause, 2)
 			_ -> raise ArgumentError, "asm supports only add"
 		end
 		quote do
 			 unquote({:+, [context: Elixir, import: Kernel], operands})
 		end
  end
```

`asm add a, b` の記述に対して仮に `a + b` のコードを生成しています。実際にはNIFのネイティブコードの生成に反映させるように作り込まないといけません。

`clause` には `quote do: asm a, b` の結果 `{:asm, [], [{:a, [], Elixir}, {:b, [], Elixir}]}` が入っています。

* `elem(clause, 0)` にはオペコード(関数名)が入ります。
* `elem(clause, 1)` には行番号を含む環境情報が入ります。
* `elem(clause, 2)` にはオペランド(引数)が入ります。

```elixir
 		operands = case elem(clause, 0) do
 			:add -> elem(clause, 2)
 			_ -> raise ArgumentError, "asm supports only add"
 		end
```

`operands` には，オペコードが `:add` だったときのみ，オペランドが入ります。オペコードが `:add` 以外だった時には仮に例外を発生させます。 

```elixir
 		quote do
 			 unquote({:+, [context: Elixir, import: Kernel], operands})
 		end
```

`operands` が `a, b` だったときに `a + b` を生成します。

この部分はもしかすると下記で十分かもしれませんね。

```elixir
 		{:+, [context: Elixir, import: Kernel], operands}
```

## when_and_int64

```elixir
  @doc """
  when_and_int64(func, module) generates the function in the context of the module with a when clause that all of arguments of the function should be int64.

  ## Examples
  	iex> Asm.when_and_int64(quote do func(a,b) end, nil) |> Macro.to_string
  	"func_ii(a, b) when is_int64(a) and is_int64(b)"
  """
  def when_and_int64(func, nil), do: when_and_int64(func, Elixir)
  def when_and_int64(func, module) do
  	quote do
  		unquote(get_func_all("i", func))
  		when unquote({:and, [context: module, import: Kernel],
  			args(func)
  			|> Enum.map(& quote do: is_int64(unquote(&1)))
  		})
  	end
  end
```

`when` 節付きの関数 `func` を生成します。

`when` 節は次のような形式をしています。

```elixir
  	{:when, [context: module],
  		[
  			func,
  			guard
  		]
  	}
  end
```

`get_func_all("i", func)` は `asm` に対して `asm_ii` という関数を生成します。(`i`の数は`func`のアリティ(引数の数)だけ生成されます)


`quote do: is_int64(a) and is_int64(b)` を実行すると下記のようになるので，

```elixir
{:and, [context: Elixir, import: Kernel],
 [
   {:is_int64, [context: Elixir, import: Asm], [{:a, [], Elixir}]},
   {:is_int64, [context: Elixir, import: Asm], [{:b, [], Elixir}]}
 ]}
```

下記のコードで，関数の引数それぞれを `x` として取り出したときに `is_int64(x)` を生成するようにします。

```elixir
  			args(func)
  			|> Enum.map(& quote do: is_int64(unquote(&1)))
```

その外側を `and` で囲みます。`context` を揃えるために引数で与えてやります。

```elixir
  		when unquote({:and, [context: module, import: Kernel],
  			args(func)
  			|> Enum.map(& quote do: is_int64(unquote(&1)))
  		})
```

# when_and_uint64, when_and_float

`when_and_int64` と同様です。

# get_name_all

```elixir
  @doc """
  get_name_all(type, func) generates a variation of a name of the function :func_ii that has the type like "i".
  """
  def get_name_all(type, func) do
  	(get_name(func) <> "_" <> (1..arity(func) |> Enum.map(fn _ -> type end) |> Enum.join()))
  	|> String.to_atom
  end
```

`type` が `"i"`，`func` が `asm/2` だったときに `:asm_ii` を返します。

`(1..arity(func) |> Enum.map(fn _ -> type end) |> Enum.join())` によって，アリティ(引数の数)分， `type` を並べます。

あとはだいたい読んだらわかるかな。

次回は[「ZEAM開発ログ v.0.4.12 マクロからコンパイルエラーやウォーニングを生成する」](https://qiita.com/zacky1972/items/bb1a9234ead45fe3189a)です。お楽しみに！