ZACKY こと山崎進です。
2018年9月28日の fukuoka.ex もくもく会で開発した成果物をお披露目します。
やりたいこと
こんなようなコードがあったとします。
def add (a, b) do
asm do
add a, b
end
end
これを次のような 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で扱うか考える」のおまけに現在研究を進めているコード生成の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
このようなコードを生成したかったんですね。
マクロを書いた
@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
が来ていたらオペランドを取り出し,そうでなかったら例外を投げます。
|> 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)
難しかったのは a + b
の生成の部分です。オペランドを子ノードにした :+
演算子をASTとして生成するコード unquote({:+, [context: Elixir, import: Kernel], &1})
で実現しました。これは iex を使って下記のように打ち込んだ結果を元に生成しました。
iex(1)> quote do: a + b
{:+, [context: Elixir, import: Kernel], [{:a, [], Elixir}, {:b, [], Elixir}]}
iex(2)>
テストコード
こんな感じのテストコードを書いてみました。
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
結果
こうなりました。
$ 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)
...
惜しい! リストを返しています。
考察
下記のような感じのインラインアセンブリコード記述が良いかなと思ったのですが,
def add (a, b) do
asm do
add a, b
end
end
関数ブロックを複数定義できるようにしないといけないので,このままだと難しいなと思うようになりました。そこで,下記のような文法にすることを検討します。
def_nif add (a, b) do
asm add a, b
end
このコードをもとに次のようにコード生成します。
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 のフロントエンドを作ってみる (黎明編)」にて,このような記述の雛形を作ってみたいと思います。