Eval of Julia code
Julia言語がコードを実行する仕組みを学ぶ上で最も難しい部分の一つは、コードのブロックを実行するためにすべての要素がどのように連携しているかを学ぶことです。
各コードのチャンクは、通常、次のような潜在的に馴染みのない名前を持つ多くのステップを経ます(順不同):flisp、AST、C++、LLVM、eval、typeinf、macroexpand、sysimg(またはシステムイメージ)、ブートストラップ、コンパイル、パース、実行、JIT、解釈、ボックス、アンボックス、内蔵関数、そしてプリミティブ関数。最終的に望ましい結果(うまくいけば)に変わります。

Julia Execution
全体プロセスの10,000フィートの概要は次のとおりです:
- ユーザーは
juliaを開始します。 cli/loader_exe.cの C 関数main()が呼び出されます。この関数はコマンドライン引数を処理し、jl_options構造体を埋め、変数ARGSを設定します。その後、Julia を初期化します(julia_initininit.cを呼び出すことによって、以前にコンパイルされた sysimg をロードする可能性があります)。最後に、Base._start()を呼び出すことによって、制御を Julia に渡します。_start()が制御を引き継ぐと、その後のコマンドのシーケンスは与えられたコマンドライン引数に依存します。たとえば、ファイル名が指定されている場合、それを実行します。そうでない場合は、インタラクティブな REPL を開始します。- ユーザーとのREPLのインタラクションに関する詳細は省略しますが、プログラムは実行したいコードのブロックを持つことになります。
- コードを実行するブロックがファイルにある場合、
jl_load(char *filename)がファイルをロードするために呼び出され、parse がそれを実行します。各コードのフラグメントはevalに渡されて実行されます。 - 各コードの断片(またはAST)は、
eval()に渡されて結果に変換されます。 eval()は各コードフラグメントを取り出し、jl_toplevel_eval_flex()で実行しようとします。jl_toplevel_eval_flex()は、コードが関数内では無効な「トップレベル」アクション(usingやmoduleなど)であるかどうかを判断します。もしそうであれば、コードをトップレベルインタープリタに渡します。jl_toplevel_eval_flex()の後に expands というコードは、マクロを排除し、ASTを「単純化」して実行しやすくするためのものです。jl_toplevel_eval_flex()は、ASTをJITコンパイルするか、直接解釈するかを決定するために、いくつかの単純なヒューリスティックを使用します。- コードを解釈する作業の大部分は
evalininterpreter.cによって処理されます。 - もし代わりにコードがコンパイルされると、作業の大部分は
codegen.cppによって処理されます。特定の引数の型のセットで初めて Julia 関数が呼び出されると、type inference がその関数で実行されます。この情報は、codegen ステップによってより高速なコードを生成するために使用されます。 - 最終的に、ユーザーはREPLを終了するか、プログラムの終わりに達し、
_start()メソッドが戻ります。 main()が終了する直前に、jl_atexit_hook(exit_code)を呼び出します。 これにより、Base._atexit()が呼び出され(これはJulia内でatexit()に登録された関数を呼び出します)、次にjl_gc_run_all_finalizers()を呼び出します。 最後に、すべてのlibuvハンドルを優雅にクリーンアップし、それらがフラッシュして閉じるのを待ちます。
Parsing
デフォルトでは、Juliaは JuliaSyntax.jl を使用してASTを生成します。歴史的には、femtolispで書かれた小さなlispプログラムを使用しており、そのソースコードはJulia内の src/flisp に配布されています。JULIA_USE_FLISP_PARSER 環境変数が 1 に設定されている場合、古いパーサーが代わりに使用されます。
Macro Expansion
eval() がマクロに遭遇すると、そのASTノードを展開してから式の評価を試みます。マクロ展開は、4d61726b646f776e2e436f64652822222c20226576616c28292229_40726566(Juliaで)から、パーサ関数 jl_macroexpand()(flispで書かれた)を経て、Juliaマクロ自体(他に何があるのか - Juliaで書かれた)に fl_invoke_julia_macro() を介して渡され、再び戻ります。
通常、マクロ展開は Meta.lower()/jl_expand() への呼び出しの最初のステップとして呼び出されますが、macroexpand()/jl_macroexpand() への呼び出しによって直接呼び出すこともできます。
Type Inference
型推論は、typeinf() in compiler/typeinfer.jlで実装されています。型推論は、Julia関数を調べ、その変数の型の範囲や関数からの戻り値の型の範囲を決定するプロセスです。これにより、既知の不変値のアンボックス化や、フィールドオフセットや関数ポインタの計算など、さまざまなランタイム操作のコンパイル時のホイストなど、多くの将来の最適化が可能になります。型推論には、定数伝播やインライン化など、他のステップも含まれる場合があります。
JIT Code Generation
Codegenは、JuliaのASTをネイティブマシンコードに変換するプロセスです。
JIT環境は、jl_init_codegen in codegen.cppへの早期呼び出しによって初期化されます。
要求に応じて、Juliaメソッドはemit_function(jl_method_instance_t*)関数によってネイティブ関数に変換されます。(注:MCJIT(LLVM v3.4+を使用)を使用する場合、各関数は新しいモジュールにJITされなければなりません。)この関数は、関数全体が出力されるまで再帰的にemit_expr()を呼び出します。
このファイルの残りの大部分は、特定のコードパターンのさまざまな手動最適化に専念しています。たとえば、emit_known_call()は、さまざまな引数の型の組み合わせに対して多くのプリミティブ関数をインライン化する方法を知っています(builtins.c)。
コード生成の他の部分は、さまざまなヘルパーファイルによって処理されます:
JIT関数のバックトレースを処理します
ccallとllvmcallFFI、およびさまざまなabi_*.cppファイルを処理します。さまざまな低レベルの内在関数の発行を処理します
System Image
システムイメージは、一連のJuliaファイルのプリコンパイルされたアーカイブです。Juliaに付属するsys.jiファイルは、そのようなシステムイメージの一例であり、ファイルsysimg.jlを実行することによって生成され、結果として得られた環境(タイプ、関数、モジュール、および定義されたすべての値を含む)をファイルにシリアライズします。したがって、これはMain、Core、およびBaseモジュールのフローズンバージョン(およびブートストラップの最後に環境にあったその他のもの)を含んでいます。このシリアライザー/デシリアライザーは、jl_save_system_image/jl_restore_system_image in staticdata.cによって実装されています。
もし sysimg ファイルが存在しない場合(jl_options.image_file == NULL)、これはコマンドラインで --build が指定されたことを意味しますので、最終的な結果は新しい sysimg ファイルになります。Julia の初期化中に、最小限の Core および Main モジュールが作成されます。その後、現在のディレクトリから boot.jl という名前のファイルが評価されます。Julia はコマンドライン引数として与えられたファイルを評価し続け、最後に達するまで続けます。最後に、結果の環境を将来の Julia 実行の出発点として使用するために "sysimg" ファイルに保存します。