4
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.10 マクロを使って micro Elixir のフロントエンドを作ってみる (試行錯誤編)

Last updated at Posted at 2018-09-28

ZACKY こと山崎進です。

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

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

やりたいこと

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

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

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

マクロを書いた

今回の開発成果物も GitHub に置いています。

  @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 のフロントエンドを作ってみる (黎明編)」にて,このような記述の雛形を作ってみたいと思います。

4
0
1

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
4
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?