3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-29

ZACKYこと山崎進です。

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

「ZEAM開発ログ 目次」はこちら

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

実現できたこと

成果物はこちら (GitHub)

こういうコードを書くと

defmodule Foo do
  require Asm
  import Asm

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

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

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

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

  @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

  @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) にはオペランド(引数)が入ります。
 		operands = case elem(clause, 0) do
 			:add -> elem(clause, 2)
 			_ -> raise ArgumentError, "asm supports only add"
 		end

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

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

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

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

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

when_and_int64

  @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 節は次のような形式をしています。

  	{: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) を実行すると下記のようになるので,

{: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) を生成するようにします。

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

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

  		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

  @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"funcasm/2 だったときに :asm_ii を返します。

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

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

次回は「ZEAM開発ログ v.0.4.12 マクロからコンパイルエラーやウォーニングを生成する」です。お楽しみに!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?