ZACKYこと山崎進です。
fukuoka.ex もくもく会が終わって帰宅後,なんだか寝付けずに未明までプログラミングを続けていて,けっこうな成果になったので報告します。
追記: context の問題があったのでデバッグしました
実現できたこと
こういうコードを書くと
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
operands
が a, 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"
,func
が asm/2
だったときに :asm_ii
を返します。
(1..arity(func) |> Enum.map(fn _ -> type end) |> Enum.join())
によって,アリティ(引数の数)分, type
を並べます。
あとはだいたい読んだらわかるかな。
次回は「ZEAM開発ログ v.0.4.12 マクロからコンパイルエラーやウォーニングを生成する」です。お楽しみに!