この記事はelixir Advent Calendar 2021の18日目の記事です。

elixirのプログラミングやphoenixに関する解説記事やドキュメントは比較的よく見かけるのですが、 elixir言語自体の解説/ドキュメント/資料は少ないと感じています。 そういうわけで最近はelixir本体をhackしたい人向けの言語の内部構造の解説ドキュメントを作成しています。

本記事では、そこでまとめたiexコマンドの起動時周辺の実装についての情報を紹介します。

(注意) この記事はelixirの1.14のバージョンを元に作成しています、 elixirのバージョンupに伴って内容が変わる可能性があります

事前準備 Link to this heading

erlangのインストール Link to this heading

公式サイト を参照してください。

elixirのソースコードの取得 Link to this heading

elixirのソースコードは こちら からcheckoutできます。

bash
1$ git clone git@github.com:elixir-lang/elixir.git
2$ cd elixir

elixirのコンパイル Link to this heading

elixirのリポジトリで以下を実行すればコンパイルとテストが走ります。

bash
1$ make clean test

コンパイルしたelixirの動作確認 Link to this heading

binの下のコマンドを実行すれば、コンパイルしたelixirを実行できます。

bash
1$ ./bin/elixir --version
2Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
3
4Elixir 1.14.0-dev (172da44) (compiled with Erlang/OTP 24)
5$

iexの起動シーケンス Link to this heading

iexの実体はerlコマンドです。 コマンドにELIXIR_CLI_DRY_RUNパラメータを付けることでコマンド実行時に何が起こっているかを見ることができます。

※ 見やすくするために改行を入れています

bash
 1$ ELIXIR_CLI_DRY_RUN=1 ./bin/iex
 2erl
 3 -pa
 4   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/eex/ebin
 5   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/elixir/ebin
 6   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/ex_unit/ebin
 7   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/iex/ebin
 8   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/logger/ebin
 9   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/mix/ebin
10 -elixir
11   ansi_enabled true
12 -noshell
13 -user
14   Elixir.IEx.CLI
15 -extra
16 --no-halt
17 +iex
18$

コマンドの実体についてはこちらの 動画資料 をご参照ください。

iexの実体であるerlに渡されるオプションですが、大きく3種類に分類できます。

erlコマンドのオプション Link to this heading

エミュレーターフラグ Link to this heading

+から始まるオプションは、エミュレータフラグ です。 このフラグはVMに渡り、VMの挙動に影響を与えます。

フラグ Link to this heading

-から始まるオプションは、フラグ です。 このフラグ情報は:init.get_argumentsで取得できます。

bash
 1$ ./bin/iex
 2Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
 3
 4Interactive Elixir (1.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
 5iex(1)> :init.get_arguments
 6[
 7  root: ['/Users/ohara_tsunenori/.asdf/installs/erlang/24.1.2'],
 8  progname: ['erl'],
 9  home: ['/Users/ohara_tsunenori'],
10  pa: ['/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/eex/ebin',
11   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/elixir/ebin',
12   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/ex_unit/ebin',
13   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/iex/ebin',
14   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/logger/ebin',
15   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/mix/ebin'],
16  elixir: ['ansi_enabled', 'true'],
17  noshell: [],
18  user: ['Elixir.IEx.CLI']
19]
20
21iex(2)>

ELIXIR_CLI_DRY_RUNを付けた際に表示された以下の引数の情報が表示されている事がわかります。

  • -pa
  • -elixir
  • -noshell
  • -user
-paフラグ Link to this heading

-paフラグは後ろに続くディレクトリのモジュールをVM起動時に読み込みます。 このフラグによってiex起動時に標準ライブラリ(eex,elixir,ex_unit,iex,logger,mix)がロードされます。

-noshellフラグ Link to this heading

-noshellフラグをつけるとVMがシェル無しで起動します。 このフラグはerlangとelixirのシェルが競合してしまう為、付与しているようです。

Plain Arguments Link to this heading

-extraは特別なフラグです。 -extraの後に続くフラグはPlain Argumentとして扱われ、 :init.get_plain_argumentsで取得できるようになります。

bash
1$ ./bin/iex
2Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
3
4Interactive Elixir (1.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
5iex(1)> :init.get_plain_arguments
6['--no-halt', '+iex']
7
8iex(2)>

ELIXIR_CLI_DRY_RUNを付けた際に表示された、-extra以降の引数の情報が表示されている事がわかります。

-userフラグの挙動 Link to this heading

-userフラグの挙動なんですが、ほとんど情報やドキュメントが無い様です。 少なくとも自分の観測範囲内では見つけられませんでした、 もし情報をお持ちの方いらっしゃいましたら教えてくれると嬉しいです。 最終的に Erlangのソースコード を読んで挙動を理解する事ができました。

結論として、この-userフラグは後ろに続くモジュールのstart関数をerl起動時に実行します。 erlコマンドは実行時にランタイムのkernelを起動するのですが、その際user_supがスーパバイザーとして起動します。 このuser_supモジュールはuserフラグがあった場合、後ろに続くモジュールのstart関数を実行します。

iexは-userフラグの後ろにElixir.IEx.CLIモジュールを指定しているので、 erl実行時に Elixir.IEx.CLI モジュールのstart関数が実行されるわけです。

-userフラグの動作実験 Link to this heading

実際に-userフラグの挙動を見ていきましょう。 サンプルとして、標準出力にメッセージを出すモジュールを-userフラグの後ろに指定して動作を確認してみます。

以下のようにメッセージを出力するstart関数をhelloモジュールに定義します。

erlang
1-module(hello).
2-export([start/0]).
3
4start() ->
5  user:start(),
6  io:put_chars("hello hacking iex!\n").

事前にhello.erlerlcでコンパイルしておきます。 コンパイルされたバイナリは拡張子.beamのファイルです。

bash
1$ erlc hello.erl
2$ ls
3hello.beam hello.erl

-userフラグの後ろにコンパイルしたhelloモジュールを指定してerlを起動した結果が以下です。 合わせてinit:get_argumentsでフラグ情報も表示しています。

bash
 1$ erl -user hello
 2hello hacking iex!
 3Eshell V12.1.2  (abort with ^G)
 4
 51> init:get_arguments().
 6[{root,["/Users/ohara_tsunenori/.asdf/installs/erlang/24.1.2"]},
 7 {progname,["erl"]},
 8 {home,["/Users/ohara_tsunenori"]},
 9 {user,["hello"]}]
10
112>

起動時に、helloモジュールのstart関数が実行されhello hacking iex!の文字列が出力されているのがわかります。 確かに-userフラグで指定しているモジュールのstart関数が実行されているようです。

userモジュールとは何か Link to this heading

helloモジュールのstart関数の中でuser:start()が実行されているのが気になった人がいるかもしれません。 io:put_charsなどの標準出力処理は userモジュール が起動していないと実行されません。

userモジュールは標準入出力に流れるメッセージに応答するI/Oサーバーを提供します。 io:put_charsはこのI/Oサーバーに対してメッセージを書き込むので、 事前にuserモジュールを起動する必要があったのです。

elixirのレベルでのエントリポイント Link to this heading

ようやくiexコマンドから Elixir.IEx.CLI モジュールのstart関数にたどり着きました。 このstart関数がelixirレベルでのiexコマンドのエントリポイントになります。

IEx.CLI.start 実行時の関数呼び出し構造 Link to this heading

IEx.CLI.start実行時にcallされるapiの全体概要は以下となります。

iex-api-flow

iexのREPLが実行される処理は大きく以下のフェーズに分類できます。

  • スーパバイザの起動
  • elixirモジュールの起動
  • IEx.Server.shell_loop
  • IEx.Evaluator.loop
  • IEx.Server.loop
  • IEx.Evaluator.eval

スーパバイザの起動 Link to this heading

iex-sup

IEx.CLI.start がcallされると、内部的に:user.start()がcallされI/Oサーバーであるuserモジュールのプロセスが生成されます。

lib/iex/lib/iex/cli.ex

elixir
 1defmodule IEx.CLI do
 2  # 〜 snip 〜
 3
 4  def start do
 5    if tty_works?() do
 6      # 〜 snip 〜
 7    else
 8      # 〜 snip 〜
 9
10      :user.start()
11
12      IEx.start([register: true] ++ options(), {:elixir, :start_cli, []})
13    end
14  end
15
16  # 〜 snip 〜
17end

またIEx.startから最終的にIEx.SupervisorがcallされIEx.ConfigIEx.BrokerIEx.Pryサーバープロセスが生成されます。

lib/iex/lib/iex/app.ex

elixir
1defmodule IEx.App do
2  # 〜 snip 〜
3
4  def start(_type, _args) do
5    children = [IEx.Config, IEx.Broker, IEx.Pry]
6    Supervisor.start_link(children, strategy: :one_for_one, name: IEx.Supervisor)
7  end
8end

iexの実行に必要なプロセスを生成した後に、IEx.Server.run_from_shelliexのREPLの実体となる処理をcallします。

elixirモジュールの起動 Link to this heading

iex-elixir-up

IEx.Server.run_from_shellspawn_monitor:elixir.start_cli()を実行するプロセスを生成し、 Iex.Server.shell_loopでメッセージを待ち受けます。

lib/iex/lib/iex/server.ex

elixir
 1defmodule IEx.Server do
 2  # 〜 snip 〜
 3
 4  # {m,f,a}={:elixir,:start_cli,[]}としてcallされる
 5  def run_from_shell(opts, {m, f, a}) do
 6    # 〜 snip 〜
 7
 8    # :elixir.start_cli() を実行するプロセスを監視付きで生成
 9    {pid, ref} = spawn_monitor(m, f, a)
10
11    # spawn_monitor 後、メッセージを待ち受ける
12    shell_loop(opts, pid, ref)
13  end
14  
15  # 〜 snip 〜
16end

spawn_monitor Link to this heading

:elixir.start_cli()spawn_monitor で生成されたプロセスで実行されます。 spawn_monitor(Mod,Fun,Args)はプロセスを監視付きで生成し、 そのプロセスの中で引数に渡した関数(Mod,Fun,Args)が実行されます。 関数の実行が完了した時、プロセスの終了メッセージを明示的に受け取る事ができます。

プロセスの終了時に受け取るメッセージは以下です。

elixir
1# 正常にプロセスが終了した場合
2{:DOWN, ref, :process, pid, :normal}
elixir
1# エラーでプロセスが終了した場合、reasonにはエラー情報が入ります
2{:DOWN, ref, :process, pid, reason}

spawn_monitorの動作実験 Link to this heading

iexspawn_monitorの動作実験をしてみましょう。 10秒sleepしてメッセージを出力する関数Foo.barを実行するプロセスを生成してみます。

elixir
1defmodule Foo do
2  def bar do
3    IO.puts "Foo#bar start"
4    :timer.sleep(10000)
5    IO.puts "sleep end"
6    :ok
7  end
8end

iex上でこのモジュールFooを定義し、Foo.bar()を実行するプロセスを生成した結果が以下です。 flushは受け取ったメッセージを表示するiexのコマンドです。

bash
 1$ ./bin/iex
 2Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
 3
 4Interactive Elixir (1.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
 5iex(1)> defmodule Foo do
 6...(1)>   def bar do
 7...(1)>     IO.puts "Foo#bar start"
 8...(1)>     :timer.sleep(10000)
 9...(1)>     IO.puts "sleep end"
10...(1)>     :ok
11...(1)>   end
12...(1)> end
13{:module, Foo,
14 <<70, 79, 82, 49, 0, 0, 5, 140, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 163,
15   0, 0, 0, 19, 10, 69, 108, 105, 120, 105, 114, 46, 70, 111, 111, 8, 95, 95,
16   105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>, {:bar, 0}}
17
18iex(2)> spawn_monitor(Foo, :bar, [])
19Foo#bar start
20{#PID<0.118.0>, #Reference<0.4069173944.326107139.44304>}
21
22iex(3)> flush
23:ok
24
25〜 10秒後 〜
26
27sleep end
28
29iex(4)> flush
30{:DOWN, #Reference<0.4069173944.326107139.44304>, :process, #PID<0.118.0>,
31 :normal}
32:ok
33
34iex(5)>

spawn_monitorでプロセスを生成後、そのプロセス内でFoo.bar()が実行されます。 10秒のsleepの後、メッセージを表示してプロセスは終了します。

上の実行結果では、sleep endのメッセージが出力された後、 flushを実行して{:DOWN, #Reference<0.4069173944.326107139.44304>, :process, #PID<0.118.0>, :normal} のメッセージを受け取っています。 メッセージは{:DOWN, ref, :process, pid, :normal}の形式なので、 正常にFoo.bar()が実行されて終了したプロセスだとわかります。

shell_loop Link to this heading

IEx.Server.shell_loop:elixir.start_cli()終了後に送信される {:DOWN, ref, :process, pid, :normal}のメッセージを受け取り、 IEx.Server.run_without_registrationをcallします。

lib/iex/lib/iex/server.ex

elixir
 1defmodule IEx.Server do
 2  # 〜 snip 〜
 3
 4  defp shell_loop(opts, pid, ref) do
 5    receive do
 6      # 〜 snip 〜
 7
 8      # :elixir.start_cli()の完了後のプロセス終了メッセージ
 9      {:DOWN, ^ref, :process, ^pid, :normal} ->
10        run_without_registration(opts)
11
12      # 〜 snip 〜
13    end
14  end
15  
16  # 〜 snip 〜
17end

IEx.Evaluator.loop Link to this heading

iex-evaluator-loop

IEx.Server.shell_loopの中でIEx.Server.run_without_registrationがcallされると、 最終的にIEx.Evaluator.loopのプロセスが立ち上がりメッセージを待ち受けます。 このIEx.Evaluator.loop{:eval, pid, code, state}の メッセージを受け取ると、code(elixirのソースコードの文字列)をevalします。 このevalの結果を{:evaled, pid, status, result}として送信元に返却した後、 IEx.Evaluator.loopをcallしてメッセージを再び待ち受けます。

lib/iex/lib/evaluator.ex

elixir
 1defmodule IEx.Evaluator do
 2  # 〜 snip 〜
 3
 4  defp loop(%{server: server, ref: ref} = state) do
 5    receive do
 6      # codeはelixirのコードの文字列
 7      {:eval, ^server, code, iex_state} ->
 8      
 9        # codeをevalする
10        {result, status, state} = eval(code, iex_state, state)
11        
12        # evalの結果を送信元のプロセスに{:evaled, ...}として返却
13        send(server, {:evaled, self(), status, result})
14        
15        # evalが終わったら再びloopでメッセージを待ち受ける
16        loop(state)
17
18      # 〜 snip 〜
19    end
20  end
21
22  # 〜 snip 〜
23end

IEx.Server.loop Link to this heading

iex-server-loop

IEx.Server.run_without_registrationは前節の通り IEx.Evaluator.loopのプロセスを生成した後、 IEx.Server.loopのプロセスを生成します。

iexのREPLの実体はこのIEx.Server.loopです。

Read Link to this heading

iex-server-loop-read

IEx.Server.loopspawnIEx.Server.io_getを実行してユーザーからの入力を受け取るプロセスを生成します。 そしてIEx.Server.wait_inputでその入力結果のメッセージを待ち受けます。

lib/iex/lib/server.ex

elixir
 1defmodule IEx.Server do
 2  # 〜 snip 〜
 3
 4  defp loop(state, prompt, evaluator, evaluator_ref) do
 5    # 〜snip〜
 6
 7    # ユーザーからの入力を受け取るプロセスを生成
 8    input = spawn(fn -> io_get(self_pid, prompt_type, prefix, counter) end)
 9
10    # 入力が終了するまで待ち受ける
11    wait_input(state, evaluator, evaluator_ref, input)
12  end
13  
14  # 〜 snip 〜
15end

IEx.Server.io_getは標準入力から入力を受け取り {:input, pid, <入力内容>} のメッセージを呼び出し元のプロセスに返却します。

lib/iex/lib/server.ex

elixir
 1defmodule IEx.Server do
 2  # 〜 snip 〜
 3
 4  defp io_get(pid, prompt_type, prefix, counter) do
 5    # 〜 snip 〜
 6
 7    # 標準入力内容をメッセージとして返却
 8    send(pid, {:input, self(), IO.gets(:stdio, prompt)})
 9  end
10
11  # 〜 snip 〜
12end

IEx.Server.wait_inputはこのメッセージを待ち受けます。

lib/iex/lib/server.ex

elixir
 1defmodule IEx.Server do
 2  # 〜 snip 〜
 3
 4  defp wait_input(state, evaluator, evaluator_ref, input) do
 5    receive do
 6      # 入力終了時の受信メッセージ
 7      {:input, ^input, code} when is_binary(code) ->
 8
 9        # 入力内容(code)をevaluatorに送信
10        # evaluatorは前節の IEx.Evaluator.loop のプロセスID
11        send(evaluator, {:eval, self(), code, state})
12      
13        # evalが終了するまで待ち受ける
14        wait_eval(state, evaluator, evaluator_ref)
15
16      # 〜 snip 〜
17    end
18  end
19  
20  # 〜 snip 〜
21end

Eval & Print Link to this heading

iex-server-loop-eval

IEx.Server.wait_inputでユーザーの入力内容を受け取ったら、 IEx.Evaluator.loopのプロセスに対して {:eval, pid, code, state}のメッセージを送信します。 そして、evalが終了するまでIEx.wait_evalでeval結果のメッセージを待ち受けます。

lib/iex/lib/server.ex

elixir
 1defmodule IEx.Server do
 2  # 〜 snip 〜
 3
 4  defp wait_eval(state, evaluator, evaluator_ref) do
 5    receive do
 6      # IEx.Evaluator.loopから返却されるeval結果
 7      {:evaled, ^evaluator, status, new_state} ->
 8      
 9        # eval結果を受け取ったら再びloopでREPLの入力を待ち受ける
10        loop(new_state, status, evaluator, evaluator_ref)
11
12      # 〜 snip 〜
13    end
14  end
15
16  # 〜 snip 〜
17end

Loop Link to this heading

iex-server-loop-print

evalの結果を受け取ったら、再びIEx.Server.loopをcallしてREPLの入力を待ち受けます。

以上がiexのREPLのループ構造です。 このループによって、iexでelixirのコードがRead、Eval、Printされていきます。

elixirコードのeval Link to this heading

iex-evaluator-loop-eval

前節で説明した通り、 IEx.Evaluator.loopのプロセスにelixirのソースコードを含む{:eval, ..., code, ...}のメッセージを送信すれば {:evaled, ..., result}としてevalの結果が返却されます。 IEx.Evaluator.loopの内部的ではIEx.Evaluator.evalがcallされます。

iexのREPLで入力されたelixirのコード(の文字列)が評価(eval)されて結果が返却されるまでに、 どういう処理がはしっているのでしょうか。

String, Charlist, Tokens, Forms, Result Link to this heading

elixirコードの文字列が評価される時、以下のようにデータが変換されます。

code-token-form-eval

CharlistからTokens、TokensからForms(Quoted)の変換とForms(Quoted)の評価は elixirモジュールの関数を呼び出して処理されます。

試しにiex1 + 1のelixirコードを順番に処理し、最終的に2という結果が取得できるか実験してみましょう。

String.to_charlist Link to this heading

elixir
1iex(1)> String.to_charlist("1 + 1")
2'1 + 1' # charlist
3iex(2)>

String.to_charlistは文字列をcharlistに変換します。

:elixir.string_to_tokens Link to this heading

elixir
 1iex(2)> :elixir.string_to_tokens(
 2iex(2)>   '1 + 1',  # charlistに変換したコード
 3iex(2)>   1,        # ソースファイル内でのコードの開始行
 4iex(2)>   1,        # ソースファイル内でのコードの開始位置
 5iex(2)>   "nofile", # ソースファイル名
 6iex(2)>   []        # option
 7iex(2)> )
 8{:ok,
 9  [
10    {:int, {1, 1, 1}, '1'},
11    {:dual_op, {1, 3, nil}, :+},
12    {:int, {1, 5, 1}, '1'}
13  ]
14}
15iex(3)>

:elixir.string_to_tokens(charlist, line, colum, file, opt)は、charlistをトークンに変換します。 この関数は以下の引数をとります。

  • charlist: elixirコードのcharlist
  • line: ソースファイル内でのコードの開始行
  • colum: ソースファイル内でのコードの開始位置
  • file: ソースファイル名
  • opt: オプション情報

:elixir.tokens_to_quoted Link to this heading

elixir
 1iex(3)> :elixir.tokens_to_quoted(
 2iex(3)>   # tokens
 3iex(3)>   [
 4iex(3)>     {:int, {1, 1, 1}, '1'},
 5iex(3)>     {:dual_op, {1, 3, nil}, :+},
 6iex(3)>     {:int, {1, 5, 1}, '1'}
 7iex(3)>   ],
 8iex(3)>   "nofile", # ソースファイル名
 9iex(3)>   [],       # option
10iex(3)> )
11{:ok,
12  {:+, [line: 1], [1, 1]}
13}
14iex(4)>

:elixir.tokens_to_quoted(tokens, file, opt)は、トークンをフォームデータに変換します。 この関数は以下の引数をとります。

  • tokens: トークン
  • file: ソースファイル名
  • opt: オプション情報

:elixir.eval_forms Link to this heading

elixir
 1iex(4)> :elixir.eval_forms(
 2iex(4)>   {:+, [line: 1], [1, 1]}, # forms
 3iex(4)>   [],                      # bindings
 4iex(4)>   []                       # env
 5iex(4)> )
 6{
 7  2,              # eval結果
 8  [],             # bindings
 9  #Macro.Env<...> # env
10}
11iex(5)>

:elixir.eval_forms(forms, bindings, env)は、フォームデータを評価して結果を返却します。 この関数は以下の引数をとります。

  • forms: フォームデータ
  • bindings: 変数の束縛情報
  • env: 環境情報

1 + 1のelixirコードの文字列から、最終的に評価結果の2が取得できました。 IEx.Evaluator.evalはこの様にしてREPLで入力されたelixirコードの文字列を評価し、 結果を取得しているのです。

IEx.Evaluator.evalの実体 Link to this heading

IEx.Evaluator.evalがelixirコードを評価する流れは以下となります。

iex-evaluator-eval

IEx.Evaluator.parse Link to this heading

IEx.Evaluator.parseでは、 :elixir.string_to_tokensをcallしてelixirコードをトークンに変換(tokenize)、 :elixir.tokens_to_quotedをcallしてトークンをフォームデータに変換(parse)します。

lib/iex/lib/evaluator.ex

elixir
 1defmodule IEx.Evaluator do
 2  # 〜 snip 〜
 3
 4  def parse(input, opts, {buffer, last_op}) do
 5    # 〜 snip 〜
 6    
 7    # stringをcharlistに変換
 8    charlist = String.to_charlist(input)
 9
10    result =
11      with # charlistをtokenに変換(tokenize)
12           {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
13           {:ok, adjusted_tokens} <- adjust_operator(tokens, line, column, file, opts, last_op),
14           # tokensをformsに変換(parse)
15           {:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
16        last_op =
17          # 〜 snip 〜
18
19        {:ok, forms, last_op}
20      end
21
22    case result do
23      # tokenize, parseが成功したら結果をformsとして返却
24      {:ok, forms, last_op} ->
25        {:ok, forms, {"", last_op}}
26
27      # 〜 snip 〜
28    end
29  end
30
31  # 〜 snip 〜
32end

IEx.Evaluator.handle_eval Link to this heading

IEx.Evaluator.handle_evalでこのフォームデータを:elixir.eval_formsをcallして評価(eval)し、結果を取得します。

lib/iex/lib/evaluator.ex

elixir
 1defmodule IEx.Evaluator do
 2  # 〜 snip 〜
 3
 4  defp handle_eval(forms, line, state) do
 5    # 〜 snip 〜
 6
 7    {result, binding, env} = :elixir.eval_forms(forms, state.binding, state.env)
 8
 9    # 〜 snip 〜
10  end
11
12  # 〜 snip 〜
13end

以上がelixirコードのevalの概要です。

iexの改造 Link to this heading

REPLで入力された文字列がparse、evalされる様子をより視覚的に理解する為に、 parse結果のフォームデータ、eval結果のbinding(変数の束縛)をそれぞれ出力するように iexを改造してみましょう。

parse結果の表示 Link to this heading

REPLの入力文字列をフォームデータに変換する処理はIEx.Evaluator.parse関数で行われていました。 このparse関数に以下のコードを追加して、フォームデータを出力するように改造します。

lib/iex/lib/evaluator.ex

追加するコード

elixir
1# ----- iex hack! -----
2IO.puts "===== forms ====="
3IO.inspect elem(result, 1) # resultの2番目の要素はforms
4# ---------------------

追加後のparse関数

elixir
 1defmodule IEx.Evaluator do
 2  # 〜 snip 〜
 3
 4  def parse(input, opts, {buffer, last_op}) do
 5    input = buffer <> input
 6    file = Keyword.get(opts, :file, "nofile")
 7    line = Keyword.get(opts, :line, 1)
 8    column = Keyword.get(opts, :column, 1)
 9    charlist = String.to_charlist(input)
10
11    result =
12      with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
13           {:ok, adjusted_tokens} <- adjust_operator(tokens, line, column, file, opts, last_op),
14           {:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
15        last_op =
16          case forms do
17            {:=, _, [_, _]} -> :match
18            _ -> :other
19          end
20
21        {:ok, forms, last_op}
22      end
23
24    # ----- iex hack! -----
25    IO.puts "===== forms ====="
26    IO.inspect elem(result, 1) # resultの2番目の要素はforms
27    # ---------------------
28
29    case result do
30      {:ok, forms, last_op} ->
31        {:ok, forms, {"", last_op}}
32
33      {:error, {_, _, ""}} ->
34        {:incomplete, {input, last_op}}
35
36      {:error, {location, error, token}} ->
37        :elixir_errors.parse_error(
38          location,
39          file,
40          error,
41          token,
42          {charlist, line, column}
43        )
44    end
45  end
46  
47  # 〜 snip 〜
48end

eval結果の表示 Link to this heading

parse関数と同様にフォームデータのevalを行うhandle_eval関数に以下のコードを追加して、 eval結果のbindings(変数の束縛情報)を表示するよう改造します。

lib/iex/lib/evaluator.ex

追加するコード

elixir
1# ----- iex hack! -----
2IO.puts "===== binding ==="
3IO.inspect binding
4IO.puts "================="
5# ---------------------

追加後のhandle_eval関数

elixir
 1defmodule IEx.Evaluator do
 2  # 〜 snip 〜
 3
 4  defp handle_eval(forms, line, state) do
 5    forms = add_if_undefined_apply_to_vars(forms)
 6    {result, binding, env} = :elixir.eval_forms(forms, state.binding, state.env)
 7
 8    # ----- iex hack! -----
 9    IO.puts "===== binding ==="
10    IO.inspect binding
11    IO.puts "================="
12    # ---------------------
13
14    unless result == IEx.dont_display_result() do
15      io_inspect(result)
16    end
17
18    state = %{state | env: env, binding: binding}
19    update_history(state, line, result)
20  end
21
22  # 〜 snip 〜
23end

改造iexの動作実験 Link to this heading

evaluator.exを変更したら、makeコマンドでelixirをリビルドします。

bash
1$ make
2==> iex (compile)
3Generated iex app
4$

変更があったiexモジュールが再コンパイルされています。 コンパイルが終わったら./bin/iexで改造したiexを起動し、elixirコードを実行してみます。

bash
 1$ ./bin/iex
 2Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
 3
 4Interactive Elixir (1.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
 5
 6iex(1)> 1 + 1
 7===== forms =====
 8{:+, [line: 1], [1, 1]}
 9===== binding ===
10[]
11=================
122
13
14iex(2)> a = 123
15===== forms =====
16{:=, [line: 2], [{:a, [line: 2], nil}, 123]}
17===== binding ===
18[a: 123]
19=================
20123
21
22iex(3)> b = [1, 2, 3]
23===== forms =====
24{:=, [line: 3], [{:b, [line: 3], nil}, [1, 2, 3]]}
25===== binding ===
26[b: [1, 2, 3], a: 123]
27=================
28[1, 2, 3]
29
30iex(4)> IO.puts "hello, hacked iex!"
31===== forms =====
32{{:., [line: 4], [{:__aliases__, [line: 4], [:IO]}, :puts]}, [line: 4],
33 ["hello, hacked iex!"]}
34hello, hacked iex!
35===== binding ===
36[b: [1, 2, 3], a: 123]
37=================
38:ok
39
40iex(5)> defmodule Hoo do; end
41===== forms =====
42{:defmodule, [line: 5],
43 [{:__aliases__, [line: 5], [:Hoo]}, [do: {:__block__, [], []}]]}
44===== binding ===
45[b: [1, 2, 3], a: 123]
46=================
47{:module, Hoo,
48 <<70, 79, 82, 49, 0, 0, 3, 232, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 129,
49   0, 0, 0, 13, 10, 69, 108, 105, 120, 105, 114, 46, 72, 111, 111, 8, 95, 95,
50   105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>, nil}
51
52iex(6)>

REPLの表示結果に、elixirコードのフォームデータとbinding情報が表示されるようになりました。 いろんなelixirコードを入力して試してみてください。

まとめ Link to this heading

iexはerlang/elixirで実装されている為、他言語で実装されたインタプリタに比べて構造が「立体的」になります。 C言語などの逐次処理をベースとした設計に比べて、複数(たくさん)のプロセスやメッセージが登場し、 処理を追ったり理解するのが比較的難しいかもしれません。 一方、Erlang/Elixirのアクターモデルによる設計/実装の面白さもあります。

iexをはじめとした、elixirのコアモジュールは読み応えがあり、記事の中で紹介した通り簡単に改造する事ができます。 これを機会にelixirをhackしてみてはいかがでしょうか?