Julia の AST
Julia には二種類のコード表現があります。一つ目は表層構文 AST (surface syntax AST) で、これはパーサー (例えば Meta.parse
) によって生成され、マクロによって操作される表現です。表層構文 AST は入力されたコードをそのまま構造的に表現したものであり、文字ストリームから julia-parser.scm
によって生成されます。二つ目のコード表現は低水準形式 (lowered form) またの名を中間表現 (intermediate representation, IR) であり、これは型推論とコード生成で使われる表現です。低水準形式に含まれるノードの種類は表層構文 AST より少なく、マクロは全て展開され、制御構造は明示的な分岐と文の並びに変換されます。低水準形式は julia-syntax.scm
によって構築されます。
マクロを書くには AST1 の理解が必要なので、こちらを先に説明します。
表層構文 AST
フロントエンド AST はほぼ全てが Expr
と原子式 (atom/シンボル/数値など) からなります。一般に、視覚的に異なる形式の構文にはそれぞれ異なるヘッドが使われます。
以降の例は S 式の構文で示されます。括弧で囲われたリストが一つの Expr
に対応し、最初の要素がヘッドに対応します。例えば (call f x)
は Julia における Expr(:call, :f, :x)
を表します。
関数呼び出し
入力 | AST |
---|---|
f(x) |
(call f x) |
f(x, y=1, z=2) |
(call f x (kw y 1) (kw z 2)) |
f(x; y=1) |
(call f (parameters (kw y 1)) x) |
f(x...) |
(call f (... x)) |
do
構文
f(x) do a,b
body
end
は、(do (call f x) (-> (tuple a b) (block body)))
とパースされます。
演算子
ほとんどの演算子は関数呼び出しを表しているだけなので、ヘッドが call
の式としてパースされます。ただし一部の演算子は (関数呼び出しとは限らない) 特殊形式であり、演算子がそのままヘッドとなります。そういった演算子は julia-parser.scm
で「構文演算子 (syntactic operator)」と呼ばれています。また N-ary としてパースされる演算子 (+
と *
) もあります。つまりこれらの演算子が続けて書かれると N 個の引数を持った関数呼び出しとしてパースされるということです。また続けて書かれた比較も特別な式構造を持ちます。
入力 | AST |
---|---|
x+y |
(call + x y) |
a+b+c+d |
(call + a b c d) |
2x |
(call * 2 x) |
a&&b |
(&& a b) |
x += 1 |
(+= x 1) |
a ? 1 : 2 |
(if a 1 2) |
a:b |
(: a b) |
a:b:c |
(: a b c) |
a,b |
(tuple a b) |
a==b |
(call == a b) |
1 < i <= n |
(comparison 1 < i <= n) |
a.b |
(. a (quote b)) |
a.(b) |
(. a (tuple b)) |
括弧を使った式
入力 | AST |
---|---|
a[i] |
(ref a i) |
t[i;j] |
(typed_vcat t i j) |
t[i j] |
(typed_hcat t i j) |
t[a b; c d] |
(typed_vcat t (row a b) (row c d)) |
a{b} |
(curly a b) |
a{b;c} |
(curly a (parameters c) b) |
[x] |
(vect x) |
[x,y] |
(vect x y) |
[x;y] |
(vcat x y) |
[x y] |
(hcat x y) |
[x y; z t] |
(vcat (row x y) (row z t)) |
[x for y in z, a in b] |
(comprehension x (= y z) (= a b)) |
T[x for y in z] |
(typed_comprehension T x (= y z)) |
(a, b, c) |
(tuple a b c) |
(a; b; c) |
(block a (block b c)) |
マクロ
入力 | AST |
---|---|
@m x y |
(macrocall @m (line) x y) |
Base.@m x y |
(macrocall (. Base (quote @m)) (line) x y) |
@Base.m x y |
(macrocall (. Base (quote @m)) (line) x y) |
文字列
入力 | AST |
---|---|
"a" |
"a" |
x"y" |
(macrocall @x_str (line) "y") |
x"y"z |
(macrocall @x_str (line) "y" "z") |
"x = $x" |
(string "x = " x) |
`a b c` |
(macrocall @cmd (line) "a b c") |
docstring の構文
"some docs"
f(x) = x
は、(macrocall (|.| Core '@doc) (line) "some docs" (= (call f x) (block x)))
とパースされます。
インポートとエクスポート
入力 | AST |
---|---|
import a |
(import (. a)) |
import a.b.c |
(import (. a b c)) |
import ...a |
(import (. . . . a)) |
import a.b, c.d |
(import (. a b) (. c d)) |
import Base: x |
(import (: (. Base) (. x))) |
import Base: x, y |
(import (: (. Base) (. x) (. y))) |
export a, b |
(export a b) |
using
の表現は import
と同じですが、ヘッドが :import
ではなく :using
となります。
数値
Julia は普通の scheme 実装より多くの数値型をサポートします。そのため AST 内で scheme の数値として表せない Julia の数値も存在します。
入力 | AST |
---|---|
11111111111111111111 |
(macrocall @int128_str (null) "11111111111111111111") |
0xfffffffffffffffff |
(macrocall @uint128_str (null) "0xfffffffffffffffff") |
1111...たくさんの桁... |
(macrocall @big_str (null) "1111....") |
ブロック形式
文のブロックは (block stmt1 stmt2 ...)
とパースされます。
if
文
if a
b
elseif c
d
else
e
end
は、次のようにパースされます:
(if a (block (line 2) b)
(elseif (block (line 3) c) (block (line 4) d)
(block (line 5 e))))
while
ループは (while condition body)
とパースされます。
for
ループは (for (= var iter) body)
とパースされます。反復するオブジェクトがいくつか指定されたときは、ブロックを使って (for (block (= v1 iter1) (= v2 iter2)) body)
とパースされます。
break
と continue
はゼロ引数の式としてパースされ、それぞれ (break)
および (continue)
となります。
let
のパースは for
ループと同様であり、(let (= var val) body)
または (let (block (= var1 val1) (= var2 val2) ...) body)
となります。
単純な関数定義は (function (call f x) body)
とパースされます。より複雑な関数定義、例えば
function f(x::T; k = 1) where T
return x+1
end
は、次のようにパースされます:
(function (where (call f (parameters (kw k 1))
(:: x T))
T)
(block (line 2) (return (call + x 1))))
型の定義
mutable struct Foo{T<:S}
x::T
end
は、次のようにパースされます。
(struct true (curly Foo (<: T S))
(block (line 2) (:: x T)))
第一引数の真偽値は定義される型が可変かどうかを表します。
try
ブロックは (try try_block var catch_block finally_block)
とパースされます。catch
の後に変数が存在しないとき var
は #f
となります。finally
節が存在しなければ最後の引数は省略されます。
クオート式
Julia ソースでコードをクオートする構文 (quote
と :(...)
) は $
のよる補間をサポートします。これは Julia のクオートが Lisp 用語で言うところの「バッククオート (backquote)」またの名を「準クオート (quasiquote)」であることを意味します。また Julia 内部では補間を行わないコードのクオートも必要になるので、Julia 内部の scheme コードは補間を行わないクオートを表すときにヘッドが inert
の式を使います。
inert
式は Julia の QuoteNode
オブジェクトに変換されます。このオブジェクトは任意の型の値を一つラップしたものであり、評価するとその値がそのまま返ります。
引数に原子式を持つ quote
式も QuoteNode
に変換されます。
行番号
ソースの位置情報は (line line_num file_name)
と表されます。第三要素は省略可能です (ファイル名が変わらないまま現在の行番号だけが変わると省略されます)。
行番号を表す式は Julia で LineNumberNode
として表されます。
マクロ
マクロの健全性はヘッドに escape
あるいは hygienic-scope
を持つ式を使って処理されます。マクロ展開の結果は自動的に (hygienic-scope block module)
で包まれ、結果が新しくできたスコープに存在することが表されます。ユーザーは (escape block)
を内側に挿入することで呼び出し側からコードを補間できます。
低水準形式
低水準形式 (IR) は型推論・最適化 (インライン化など)・コード生成で使われるので、コンパイラにとって AST より重要です。一方で、低水準形式は入力された構文を大幅に組み替えることで生成されるので、人間にとっては AST より読みにくくなります。
Symbol
と数値型に加えて、低水準形式には次のデータ型が現れます:
-
Expr
head
フィールドがノードの種類を表し、Vector{Any}
型のargs
フィールドが部分式を表します。表層構文 AST ではほぼ全ての部分がExpr
ですが、IR ではそれほど使われません。多くの場合 IR に含まれるExpr
は関数呼び出し・条件分岐 (gotoifnot
)・関数からの値の返却のいずれかです。 -
Slot
引数とローカル変数を連続した番号で識別します。
Slot
は抽象型であり、SlotNumber
とTypedSlot
を部分型に持ちます。この二つの型は整数フィールドid
を持ち、この値がスロットの添え字を表します。たいていのスロットは複数回使われる場合でも同じ型として使われますが、そういったスロットはSlotNumber
で表されます。こういったスロットの型はCodeInfo
2 オブジェクトのslottypes
に保存されます。使われるたびに型注釈が必要なスロットはTypedSlot
で表され、この型はtyp
フィールドを持ちます。 -
CodeInfo
文の集合の IR をラップします。
code
フィールドが実行する式の配列です。 -
GotoNode
無条件分岐を表します。引数はコード配列に対する添え字であり、分岐先を表します。
-
QuoteNode
データとして参照するために任意の値をラップします。例えば関数
f() = :a
はシンボルを評価せずに返すので、その低水準形式にはシンボルa
をvalue
フィールドに持つQuoteNode
が含まれます。 -
GlobalRef
モジュール
mod
のグローバル変数name
に対する参照です。 -
SSAValue
静的単一代入 (static single assignment, SSA) 変数を参照します。SSA 変数はコンパイラによって挿入され、そのとき連続した番号が付与されます。
SSAValue
の番号 (id
) は変数が表す値を持つ式のコード配列における添え字です。 -
NewvarNode
変数 (スロット) が作成される点を示す印を付けます。これは変数を未定義状態にリセットする効果を持ちます。
Expr
型
Expr
型の head
フィールドに使われるシンボルとその意味は次の通りです:
-
call
関数呼び出し (動的ディスパッチ) です。
args[1]
が呼び出される関数で、args[2:end]
が関数に対する引数です。 -
invoke
関数呼び出し (静的ディスパッチ) です。
args[1]
が呼び出されるMethodInstance
で、args[2:end]
はその引数 (呼び出される関数はargs[2]
) です。 -
static_parameter
静的パラメータ (メソッドが持つ型変数など) に対する、添え字を使った参照です。
-
gotoifnot
条件分岐です。
args[1]
がfalse
なら実行をargs[2]
が指す添え字に移動します。 -
=
代入です。IR では、第一引数が必ず
Slot
またはGlobalRef
になります。 -
method
総称関数にメソッドを追加し、必要なら結果を代入します。
一引数の形式と三引数の形式があります。一引数の形式は
function foo end
という構文から生成され、引数はシンボルとなります。このシンボルが現在のスコープで関数の名前となっているなら、何も起こりません。このシンボルが未定義なら、新しい関数が作成されてシンボルが表す識別子に代入されます。このシンボルが関数でない値が代入された識別子の名前となっているなら、エラーが発生します。ここでシンボルが「関数の名前となっている」とは、シンボルの表す識別子がシングルトン型のオブジェクトを指す定数束縛であることとして定義されます。こうなっているのは、シングルトン型のインスタンスがメソッドを追加する型を曖昧さなく指定するためです。型がフィールドを持っていると、メソッドがインスタンスに加わるのか型に加わるのかが判断できなくなります。三引数の形式は次の引数を持ちます:
-
args[1]
関数の名前です。名前が不明なら
false
となります。この引数がシンボルなら、式はまず上述した一引数の形式のように振る舞います。この引数はそれ以降は使われません。この引数がfalse
なら、厳密に型だけを使って ((::T)(x) = x
などとして) メソッドが追加されていることを意味します。 -
args[2]
引数の型を表すデータからなる
SimpleVector
です。args[2][1]
は引数型からなるSimpleVector
であり、args[2][2]
はメソッドの静的パラメータに対応する型変数からなるSimpleVector
です。 -
args[3]
メソッド自身の
CodeInfo
です。"スコープ外" のメソッド定義 (異なるスコープで定義されたメソッドを持つ関数へのメソッドの追加) では、args[3]
は:lambda
式に評価される式となります。
-
-
struct_type
新しい
struct
を定義する七引数の式です:-
args[1]
struct
の名前です。 -
args[2]
パラメータを示す
SimpleVector
を作成するcall
式です。 -
args[3]
フィールドの名前を示す
SimpleVector
を作成するcall
式です。 -
args[4]
上位型を示す
Symbol
,GlobalRef
,Expr
のいずれかです。:Integer
,GlobalRef(Core, :Any)
,:(Core.apply_type(AbstractArray, T, N))
などとなります。 -
args[5]
フィールドの型を示す
SimpleVector
を作成するcall
式です。 -
args[6]
真偽値です。
mutable
ならtrue
となります。 -
args[7]
初期化する引数の個数です。フィールドの個数、もしくは内部コンストラクタの
new
文で初期化すべきフィールドの最小個数です。
-
-
abstract_type
新しい抽象型を定義する三引数の式です。引数は
struct_type
式の第一・第二・第四引数と同様です。 -
primitive_type
新しいプリミティブ型を定義する三引数の式です。引数は
struct_type
式の第一・第二・第四引数と同様です。第三引数がビット数を表します。Julia 1.5struct_type
,abstract_type
,primitive_type
は Julia 1.5 で削除され、新しい組み込み関数の呼び出しで置き換えられました。 -
global
グローバル束縛を宣言します。
-
const
(グローバルな) 変数を定数と宣言します。
-
new
構造体風のオブジェクトをアロケートします。第一引数は型です。疑似関数
new
は低水準形式でこの式となり、そのとき型がコンパイラによって必ず挿入されます。これは内部でだけ使われる機能であり、型の検査は行われません。適当に作ったnew
式を評価すると簡単に segfault します。 -
splatnew
new
と同様ですが、フィールド値が単一のタプルで渡されます。splatnew
という名前が付いているのは、new
をファーストクラスの関数とみなせばBase.splat(new)
と同じ動作をするためです。 -
return
この式を含む関数の値として引数を返します。
-
isdefined
Expr(:isdefined, :x)
は現在のスコープでx
が定義されているかどうかを示す真偽値を返します。 -
the_exception
catch
ブロック内で捕捉された例外に評価されます。jl_current_exception()
が返す値と同じです。 -
enter
例外ハンドラに入ります (
setjmp
を設定します)。args[1]
はエラーが発生したときにジャンプするcatch
ブロックのラベルです。pop_exception
が消費するトークンを生成します。 -
leave
例外ハンドラをポップします。
args[1]
はポップするハンドラの個数です。 -
pop_exception
catch
ブロックを離れるときに、対応するenter
に入ったときの状態まで現在の例外スタックをポップします。args[1]
には対応するenter
からのトークンが含まれます。Julia 1.1pop_exception
は Julia 1.1 で追加されました。 -
inbounds
境界検査を有効化または無効化します。状態はスタックで管理されます: この式の第一引数が
true
(境界検査を無効化) またはfalse
(境界検査を有効化) だと、その値がスタックにプッシュされます。第一引数が:pop
だと、スタックから値がポップされます。 -
boundscheck
@inbounds
の印が付いたコードセクションにインライン化されるときfalse
となり、それ以外のときtrue
となります。 -
loopinfo
ループの末尾を示す印を付けます。
@simd
式の内部ループを示す印を付けるため、もしくは LLVM ループパスに情報を伝えるためにLowerSimdLoop
へ渡されるメタデータを含みます。 -
copyast
準クオートの実装の一部です。引数は表層構文 AST であり、実行時にはそれが再帰的にコピーされて返ります。
-
meta
メタデータです。通常
args[1]
はメタデータの種類を示すシンボルであり、その後は自由形式です。次のメタデータがよく使われます::inline
,:noinline
: インライン化に関するヒントです。
-
foreigncall
ccall
に関する情報を含んだ静的に計算されるコンテナです。フィールドは次の通りです:-
args[1]
外部関数を使うときにパースされる式です。関数の名前を表します。
-
args[2]::Type
返り値の型 (リテラル) です。呼び出しを含むメソッドが定義されるときに静的に計算されます。
-
args[3]::SimpleVector
引数型のベクトル (リテラル) です。呼び出しを含むメソッドが定義されるときに静的に計算されます。
-
args[4]::Int
可変長引数の定義で必要な引数の個数です。
-
args[5]::QuoteNode{Symbol}
呼び出しで使う呼び出し規約です。
-
args[6:length(args[3])]
全ての引数に対する値です (
args[3]
が指定する型を持ちます)。 -
args[(length(args[3]) + 1):end]
呼び出しの間に GC ルートが必要な可能性のある追加のオブジェクトです。この式がいつ出現しどのように処理されるかについては LLVM の処理の章を参照してください。
-
Method
型
単一のメソッドを曖昧さなく記述する共有メタデータからなるコンテナです3。
-
name
,module
,file
,line
,sig
コンピューターおよび人間のためのメタデータです。メソッドを一意に識別します。
-
ambig
このメソッドと曖昧性が生じる可能性がある他のメソッドのキャッシュです。
-
specializations
このメソッドに対してこれまでに作成された
MethodInstance
全てのキャッシュです。唯一性を保証するために使われます。唯一性は効率のため、特にメソッド無効化の追跡と漸進事前コンパイルの効率のために必要です。 -
source
オリジナルのソースコードです。利用可能な場合のみ設定され、通常は圧縮されます。
-
generator
実行すると「特定のメソッドシグネチャに対して特殊化されたソース」が得られる呼び出し可能オブジェクトです。
-
roots
AST に補間された AST でないものへのポインタです。AST の圧縮・型推論・ネイティブコードの生成で必要になります。
-
nargs
,isva
,called
,isstaged
,pure
この
Method
のソースコードに対する説明を加えるビットフィールドです。 -
primary_world
この
Method
を "保有する" 世界時です。
MethodInstance
型
Method
に対する呼び出し可能な単一のシグネチャを表す曖昧性が取り除かれたコンテナです。マルチスレッディングにおけるロックの適切な管理と対処の章に MethodInstance
型のフィールドを安全な形で変更するときの重要な注意点があります。
-
specTypes
この
MethodInstance
に対するプライマリキーです。唯一性はdef.specializations
の探索によって保証されます。 -
def
この
MethodInstance
が特殊化を記述しているMethod
です。このMethodInstance
がモジュールのトップレベルで展開されたLambda
であり、かつMethod
の一部でない場合にはモジュールとなります。 -
sparam_vals
specTypes
に含まれる静的パラメータの値です。def.sparam_syms
を添え字に持ちます。Method.unspecialized
であるMethodInstance
に対しては、このフィールドは空のSimpleVector
となります。MethodTable
キャッシュからの実行時MethodInstance
に対しては、このフィールドは必ず存在して添え字アクセス可能です。 -
uninferred
トップレベルサンクの圧縮されていないソースコードです。被生成関数に対しては、ソースコードが存在する可能性のある複数の場所のいずれかとなります。
-
backedges
新しいメソッドが定義されたときに必要になる可能性のある漸進的な再解析/再コンパイルを効率的に進めるために、この
MethodInstance
に依存するメソッドのキャッシュのリストがここに格納されます。推論あるいは最適化によってこのMethodInstance
を呼び出すことが判明した他のMethodInstance
のリスト保持するということです。最適化の結果はcache
のどこかに保存されることもあれば、定数伝播のようなキャッシュが必要ない結果であることもあります。そこで様々なキャッシュエントリーに対する後方辺を全てこのフィールドにまとめるようになっています (いずれにせよ、ほとんどの場合で適用可能なキャッシュエントリーはmax_world
に対する門番値を持ったものが一つあるだけです)。 -
cache
この
MethodInstance
が表すテンプレートのインスタンス化を共有するCodeInstance
オブジェクトのキャッシュです。
CodeInstance
型
-
def
このキャッシュエントリーの元となった
MethodInstance
です。
-
rettype
/rettype_const
specFunctionObject
フィールドに対して推論された返り値の型です。多くの場合、関数に対して計算された返り値と一致します。 -
inferred
この関数に対して推論を行った後のソースのキャッシュが含まれる場合があります。
rettype
が推論されたことを示すためにnothing
となる場合もあります。 -
ftpr
一般的な
jlcall
のエントリーポイントです。 -
jlcall_api
fptr
を呼ぶときに使う ABI です。一部の重要な値を示します:0
: コンパイルされていない1
:JL_CALLABLE
(jl_value_t *(*)(jl_function_t *f, jl_value_t *args[nargs], uint32_t nargs)
)2
: 定数 (rettype_const
に格納された値)3
: 静的パラメータが埋め込まれている (jl_value_t *(*)(jl_svec_t *sparams, jl_function_t *f, jl_value_t *args[nargs], uint32_t nargs)
)4
: インタープリタ上での実行 (jl_value_t *(*)(jl_method_instance_t *meth, jl_function_t *f, jl_value_t *args[nargs], uint32_t nargs)
)
-
min_world
/max_world
このメソッドインスタンスを正当に呼び出せる世界時の区間を表します。
max_world
が未知な場合は特別な値-1
となります。
CodeInfo
型
低水準化されたソースコードを保持する (通常は一時的な) コンテナです。
-
code
文からなる要素型
Any
の配列です。 -
slotnames
各スロット (引数あるいはローカル変数) に対する名前を与えるシンボルの配列です。
-
slotflags
スロットのプロパティを示す
UInt8
の配列です。プロパティはビットフラグとして表されます:2
: 代入されているかどうか (対応するスロットが表す変数を左辺に持つ代入文が存在しないときに限ってfalse
となる)8
: 定数かどうか (このフラグはローカル変数に対して現在使われていない)16
: 静的に一度だけ代入されるかどうか32
: 代入される前に使われる可能性があるかどうか (型推論の後でだけ有効)
-
ssavaluetypes
配列もしくは
Int
です。Int
なら、コンパイラがこの関数に挿入した一時的なロケーションの個数 (code
配列の長さ) を与えます。配列なら、各ロケーションの型を指定します。 -
ssaflags
関数内の式それぞれに対する文レベルのフラグです。多くのビットは予約されていますが、まだ実装で使われていません:
- 0 = 境界内アクセス
- 1,2 = <予約済み> インラインヒント/常にインライン/インラインしない
- 3 = <予約済み> 厳密な IEEE (strictfp)
- 4-6 = <未使用>
- 7 = <予約済み> アウトオブバンドに関する情報
-
linetable
ソースロケーションオブジェクトの配列です。
-
codelocs
linetable
への整数添え字からなる配列です。各文に関連付いたロケーションを与えます。
省略可能なフィールド:
-
slottypes
スロットの型の配列です。
-
rettype
低水準形式 (IR) の推論された返り値です。デフォルト値は
Any
です。 -
method_for_inference_limit_heuristics
method_for_inference_heuristics
は与えられたメソッドのジェネレータを推論で必要なったときに展開します。 -
parent
このオブジェクトを "保有" する
MethodInstance
です (存在する場合のみ)。 -
min_world
/max_world
推論が行われた時点においてこのコードが有効だった世界時の区間です。
真偽値プロパティ:
-
inferred
このコードが型推論によって生成されたかどうかを表します。
-
inlineable
このコードがインライン化の対象かどうかを表します。
-
propagate_inbounds
このコードが
@boundscheck
ブロックを削除するために@inbounds
を伝播させるかどうかを表します。 -
pure
この関数が引数の純粋関数であることが分かっているかどうかを表します。純粋性はメソッドキャッシュの状態と可変なグローバル状態に関して判断されます。