Contents

iex inside

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

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

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

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

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

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

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

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

$ make clean test

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

$ ./bin/elixir --version
Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Elixir 1.14.0-dev (172da44) (compiled with Erlang/OTP 24)
$

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

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

$ ELIXIR_CLI_DRY_RUN=1 ./bin/iex
erl
 -pa
   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/eex/ebin
   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/elixir/ebin
   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/ex_unit/ebin
   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/iex/ebin
   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/logger/ebin
   /Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/mix/ebin
 -elixir
   ansi_enabled true
 -noshell
 -user
   Elixir.IEx.CLI
 -extra
 --no-halt
 +iex
$

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

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

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

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

$ ./bin/iex
Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Interactive Elixir (1.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> :init.get_arguments
[
  root: ['/Users/ohara_tsunenori/.asdf/installs/erlang/24.1.2'],
  progname: ['erl'],
  home: ['/Users/ohara_tsunenori'],
  pa: ['/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/eex/ebin',
   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/elixir/ebin',
   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/ex_unit/ebin',
   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/iex/ebin',
   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/logger/ebin',
   '/Users/ohara_tsunenori/Git/github.com/ohr486/elixir/bin/../lib/mix/ebin'],
  elixir: ['ansi_enabled', 'true'],
  noshell: [],
  user: ['Elixir.IEx.CLI']
]

iex(2)>

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

  • -pa
  • -elixir
  • -noshell
  • -user

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

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

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

$ ./bin/iex
Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Interactive Elixir (1.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> :init.get_plain_arguments
['--no-halt', '+iex']

iex(2)>

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

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

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

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

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

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

-module(hello).
-export([start/0]).

start() ->
  user:start(),
  io:put_chars("hello hacking iex!\n").

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

$ erlc hello.erl
$ ls
hello.beam hello.erl

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

$ erl -user hello
hello hacking iex!
Eshell V12.1.2  (abort with ^G)

1> init:get_arguments().
[{root,["/Users/ohara_tsunenori/.asdf/installs/erlang/24.1.2"]},
 {progname,["erl"]},
 {home,["/Users/ohara_tsunenori"]},
 {user,["hello"]}]

2>

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

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

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

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

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

iex-api-flow

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

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

iex-sup

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

lib/iex/lib/iex/cli.ex

defmodule IEx.CLI do
  # 〜 snip 〜

  def start do
    if tty_works?() do
      # 〜 snip 〜
    else
      # 〜 snip 〜

      :user.start()

      IEx.start([register: true] ++ options(), {:elixir, :start_cli, []})
    end
  end

  # 〜 snip 〜
end

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

lib/iex/lib/iex/app.ex

defmodule IEx.App do
  # 〜 snip 〜

  def start(_type, _args) do
    children = [IEx.Config, IEx.Broker, IEx.Pry]
    Supervisor.start_link(children, strategy: :one_for_one, name: IEx.Supervisor)
  end
end

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

iex-elixir-up

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

lib/iex/lib/iex/server.ex

defmodule IEx.Server do
  # 〜 snip 〜

  # {m,f,a}={:elixir,:start_cli,[]}としてcallされる
  def run_from_shell(opts, {m, f, a}) do
    # 〜 snip 〜

    # :elixir.start_cli() を実行するプロセスを監視付きで生成
    {pid, ref} = spawn_monitor(m, f, a)

    # spawn_monitor 後、メッセージを待ち受ける
    shell_loop(opts, pid, ref)
  end
  
  # 〜 snip 〜
end

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

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

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

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

defmodule Foo do
  def bar do
    IO.puts "Foo#bar start"
    :timer.sleep(10000)
    IO.puts "sleep end"
    :ok
  end
end

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

$ ./bin/iex
Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Interactive Elixir (1.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule Foo do
...(1)>   def bar do
...(1)>     IO.puts "Foo#bar start"
...(1)>     :timer.sleep(10000)
...(1)>     IO.puts "sleep end"
...(1)>     :ok
...(1)>   end
...(1)> end
{:module, Foo,
 <<70, 79, 82, 49, 0, 0, 5, 140, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 163,
   0, 0, 0, 19, 10, 69, 108, 105, 120, 105, 114, 46, 70, 111, 111, 8, 95, 95,
   105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>, {:bar, 0}}

iex(2)> spawn_monitor(Foo, :bar, [])
Foo#bar start
{#PID<0.118.0>, #Reference<0.4069173944.326107139.44304>}

iex(3)> flush
:ok

〜 10秒後 〜

sleep end

iex(4)> flush
{:DOWN, #Reference<0.4069173944.326107139.44304>, :process, #PID<0.118.0>,
 :normal}
:ok

iex(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()が実行されて終了したプロセスだとわかります。

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

lib/iex/lib/iex/server.ex

defmodule IEx.Server do
  # 〜 snip 〜

  defp shell_loop(opts, pid, ref) do
    receive do
      # 〜 snip 〜

      # :elixir.start_cli()の完了後のプロセス終了メッセージ
      {:DOWN, ^ref, :process, ^pid, :normal} ->
        run_without_registration(opts)

      # 〜 snip 〜
    end
  end
  
  # 〜 snip 〜
end

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

defmodule IEx.Evaluator do
  # 〜 snip 〜

  defp loop(%{server: server, ref: ref} = state) do
    receive do
      # codeはelixirのコードの文字列
      {:eval, ^server, code, iex_state} ->
      
        # codeをevalする
        {result, status, state} = eval(code, iex_state, state)
        
        # evalの結果を送信元のプロセスに{:evaled, ...}として返却
        send(server, {:evaled, self(), status, result})
        
        # evalが終わったら再びloopでメッセージを待ち受ける
        loop(state)

      # 〜 snip 〜
    end
  end

  # 〜 snip 〜
end

iex-server-loop

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

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

iex-server-loop-read

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

lib/iex/lib/server.ex

defmodule IEx.Server do
  # 〜 snip 〜

  defp loop(state, prompt, evaluator, evaluator_ref) do
    # 〜snip〜

    # ユーザーからの入力を受け取るプロセスを生成
    input = spawn(fn -> io_get(self_pid, prompt_type, prefix, counter) end)

    # 入力が終了するまで待ち受ける
    wait_input(state, evaluator, evaluator_ref, input)
  end
  
  # 〜 snip 〜
end

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

lib/iex/lib/server.ex

defmodule IEx.Server do
  # 〜 snip 〜

  defp io_get(pid, prompt_type, prefix, counter) do
    # 〜 snip 〜

    # 標準入力内容をメッセージとして返却
    send(pid, {:input, self(), IO.gets(:stdio, prompt)})
  end

  # 〜 snip 〜
end

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

lib/iex/lib/server.ex

defmodule IEx.Server do
  # 〜 snip 〜

  defp wait_input(state, evaluator, evaluator_ref, input) do
    receive do
      # 入力終了時の受信メッセージ
      {:input, ^input, code} when is_binary(code) ->

        # 入力内容(code)をevaluatorに送信
        # evaluatorは前節の IEx.Evaluator.loop のプロセスID
        send(evaluator, {:eval, self(), code, state})
      
        # evalが終了するまで待ち受ける
        wait_eval(state, evaluator, evaluator_ref)

      # 〜 snip 〜
    end
  end
  
  # 〜 snip 〜
end

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

defmodule IEx.Server do
  # 〜 snip 〜

  defp wait_eval(state, evaluator, evaluator_ref) do
    receive do
      # IEx.Evaluator.loopから返却されるeval結果
      {:evaled, ^evaluator, status, new_state} ->
      
        # eval結果を受け取ったら再びloopでREPLの入力を待ち受ける
        loop(new_state, status, evaluator, evaluator_ref)

      # 〜 snip 〜
    end
  end

  # 〜 snip 〜
end

iex-server-loop-print

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

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

iex-evaluator-loop-eval

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

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

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

code-token-form-eval

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

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

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

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

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

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

  • charlist: elixirコードのcharlist
  • line: ソースファイル内でのコードの開始行
  • colum: ソースファイル内でのコードの開始位置
  • file: ソースファイル名
  • opt: オプション情報
iex(3)> :elixir.tokens_to_quoted(
iex(3)>   # tokens
iex(3)>   [
iex(3)>     {:int, {1, 1, 1}, '1'},
iex(3)>     {:dual_op, {1, 3, nil}, :+},
iex(3)>     {:int, {1, 5, 1}, '1'}
iex(3)>   ],
iex(3)>   "nofile", # ソースファイル名
iex(3)>   [],       # option
iex(3)> )
{:ok,
  {:+, [line: 1], [1, 1]}
}
iex(4)>

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

  • tokens: トークン
  • file: ソースファイル名
  • opt: オプション情報
iex(4)> :elixir.eval_forms(
iex(4)>   {:+, [line: 1], [1, 1]}, # forms
iex(4)>   [],                      # bindings
iex(4)>   []                       # env
iex(4)> )
{
  2,              # eval結果
  [],             # bindings
  #Macro.Env<...> # env
}
iex(5)>

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

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

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

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

iex-evaluator-eval

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

lib/iex/lib/evaluator.ex

defmodule IEx.Evaluator do
  # 〜 snip 〜

  def parse(input, opts, {buffer, last_op}) do
    # 〜 snip 〜
    
    # stringをcharlistに変換
    charlist = String.to_charlist(input)

    result =
      with # charlistをtokenに変換(tokenize)
           {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
           {:ok, adjusted_tokens} <- adjust_operator(tokens, line, column, file, opts, last_op),
           # tokensをformsに変換(parse)
           {:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
        last_op =
          # 〜 snip 〜

        {:ok, forms, last_op}
      end

    case result do
      # tokenize, parseが成功したら結果をformsとして返却
      {:ok, forms, last_op} ->
        {:ok, forms, {"", last_op}}

      # 〜 snip 〜
    end
  end

  # 〜 snip 〜
end

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

lib/iex/lib/evaluator.ex

defmodule IEx.Evaluator do
  # 〜 snip 〜

  defp handle_eval(forms, line, state) do
    # 〜 snip 〜

    {result, binding, env} = :elixir.eval_forms(forms, state.binding, state.env)

    # 〜 snip 〜
  end

  # 〜 snip 〜
end

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

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

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

lib/iex/lib/evaluator.ex

追加するコード

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

追加後のparse関数

defmodule IEx.Evaluator do
  # 〜 snip 〜

  def parse(input, opts, {buffer, last_op}) do
    input = buffer <> input
    file = Keyword.get(opts, :file, "nofile")
    line = Keyword.get(opts, :line, 1)
    column = Keyword.get(opts, :column, 1)
    charlist = String.to_charlist(input)

    result =
      with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
           {:ok, adjusted_tokens} <- adjust_operator(tokens, line, column, file, opts, last_op),
           {:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
        last_op =
          case forms do
            {:=, _, [_, _]} -> :match
            _ -> :other
          end

        {:ok, forms, last_op}
      end

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

    case result do
      {:ok, forms, last_op} ->
        {:ok, forms, {"", last_op}}

      {:error, {_, _, ""}} ->
        {:incomplete, {input, last_op}}

      {:error, {location, error, token}} ->
        :elixir_errors.parse_error(
          location,
          file,
          error,
          token,
          {charlist, line, column}
        )
    end
  end
  
  # 〜 snip 〜
end

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

lib/iex/lib/evaluator.ex

追加するコード

# ----- iex hack! -----
IO.puts "===== binding ==="
IO.inspect binding
IO.puts "================="
# ---------------------

追加後のhandle_eval関数

defmodule IEx.Evaluator do
  # 〜 snip 〜

  defp handle_eval(forms, line, state) do
    forms = add_if_undefined_apply_to_vars(forms)
    {result, binding, env} = :elixir.eval_forms(forms, state.binding, state.env)

    # ----- iex hack! -----
    IO.puts "===== binding ==="
    IO.inspect binding
    IO.puts "================="
    # ---------------------

    unless result == IEx.dont_display_result() do
      io_inspect(result)
    end

    state = %{state | env: env, binding: binding}
    update_history(state, line, result)
  end

  # 〜 snip 〜
end

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

$ make
==> iex (compile)
Generated iex app
$

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

$ ./bin/iex
Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Interactive Elixir (1.14.0-dev) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> 1 + 1
===== forms =====
{:+, [line: 1], [1, 1]}
===== binding ===
[]
=================
2

iex(2)> a = 123
===== forms =====
{:=, [line: 2], [{:a, [line: 2], nil}, 123]}
===== binding ===
[a: 123]
=================
123

iex(3)> b = [1, 2, 3]
===== forms =====
{:=, [line: 3], [{:b, [line: 3], nil}, [1, 2, 3]]}
===== binding ===
[b: [1, 2, 3], a: 123]
=================
[1, 2, 3]

iex(4)> IO.puts "hello, hacked iex!"
===== forms =====
{{:., [line: 4], [{:__aliases__, [line: 4], [:IO]}, :puts]}, [line: 4],
 ["hello, hacked iex!"]}
hello, hacked iex!
===== binding ===
[b: [1, 2, 3], a: 123]
=================
:ok

iex(5)> defmodule Hoo do; end
===== forms =====
{:defmodule, [line: 5],
 [{:__aliases__, [line: 5], [:Hoo]}, [do: {:__block__, [], []}]]}
===== binding ===
[b: [1, 2, 3], a: 123]
=================
{:module, Hoo,
 <<70, 79, 82, 49, 0, 0, 3, 232, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 129,
   0, 0, 0, 13, 10, 69, 108, 105, 120, 105, 114, 46, 72, 111, 111, 8, 95, 95,
   105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>, nil}

iex(6)>

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

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

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

Комментарии