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 と数値型に加えて、低水準形式には次のデータ型が現れます:
-
Exprheadフィールドがノードの種類を表し、Vector{Any}型のargsフィールドが部分式を表します。表層構文 AST ではほぼ全ての部分がExprですが、IR ではそれほど使われません。多くの場合 IR に含まれるExprは関数呼び出し・条件分岐 (gotoifnot)・関数からの値の返却のいずれかです。 -
Slot引数とローカル変数を連続した番号で識別します。
Slotは抽象型であり、SlotNumberとTypedSlotを部分型に持ちます。この二つの型は整数フィールドidを持ち、この値がスロットの添え字を表します。たいていのスロットは複数回使われる場合でも同じ型として使われますが、そういったスロットはSlotNumberで表されます。こういったスロットの型はCodeInfo2 オブジェクトの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 します。 -
splatnewnewと同様ですが、フィールド値が単一のタプルで渡されます。splatnewという名前が付いているのは、newをファーストクラスの関数とみなせばBase.splat(new)と同じ動作をするためです。 -
returnこの式を含む関数の値として引数を返します。
-
isdefinedExpr(:isdefined, :x)は現在のスコープでxが定義されているかどうかを示す真偽値を返します。 -
the_exceptioncatchブロック内で捕捉された例外に評価されます。jl_current_exception()が返す値と同じです。 -
enter例外ハンドラに入ります (
setjmpを設定します)。args[1]はエラーが発生したときにジャンプするcatchブロックのラベルです。pop_exceptionが消費するトークンを生成します。 -
leave例外ハンドラをポップします。
args[1]はポップするハンドラの個数です。 -
pop_exceptioncatchブロックを離れるときに、対応する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: インライン化に関するヒントです。
-
foreigncallccallに関する情報を含んだ静的に計算されるコンテナです。フィールドは次の通りです:-
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実行すると「特定のメソッドシグネチャに対して特殊化されたソース」が得られる呼び出し可能オブジェクトです。
-
rootsAST に補間された 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_valsspecTypesに含まれる静的パラメータの値です。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_constspecFunctionObjectフィールドに対して推論された返り値の型です。多くの場合、関数に対して計算された返り値と一致します。 -
inferredこの関数に対して推論を行った後のソースのキャッシュが含まれる場合があります。
rettypeが推論されたことを示すためにnothingとなる場合もあります。 -
ftpr一般的な
jlcallのエントリーポイントです。 -
jlcall_apifptrを呼ぶときに使う 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ソースロケーションオブジェクトの配列です。
-
codelocslinetableへの整数添え字からなる配列です。各文に関連付いたロケーションを与えます。
省略可能なフィールド:
-
slottypesスロットの型の配列です。
-
rettype低水準形式 (IR) の推論された返り値です。デフォルト値は
Anyです。 -
method_for_inference_limit_heuristicsmethod_for_inference_heuristicsは与えられたメソッドのジェネレータを推論で必要なったときに展開します。 -
parentこのオブジェクトを "保有" する
MethodInstanceです (存在する場合のみ)。 -
min_world/max_world推論が行われた時点においてこのコードが有効だった世界時の区間です。
真偽値プロパティ:
-
inferredこのコードが型推論によって生成されたかどうかを表します。
-
inlineableこのコードがインライン化の対象かどうかを表します。
-
propagate_inboundsこのコードが
@boundscheckブロックを削除するために@inboundsを伝播させるかどうかを表します。 -
pureこの関数が引数の純粋関数であることが分かっているかどうかを表します。純粋性はメソッドキャッシュの状態と可変なグローバル状態に関して判断されます。