LLVM におけるコルーチン

警告

LLVM リリース間の互換性は保証されていません。

導入

LLVM コルーチンは、1 つ以上のサスペンドポイントを持つ関数です。サスペンドポイントに到達すると、コルーチンの実行が中断され、制御が呼び出し元に戻ります。中断されたコルーチンは、最後のサスペンドポイントから実行を継続するために再開するか、破棄することができます。

次の例では、サスペンドされたコルーチンのハンドル(コルーチンハンドル)を返す関数f(それ自体がコルーチンである可能性もあれば、そうでない可能性もある)を呼び出しています。このハンドルは、main でコルーチンを 2 回再開し、その後破棄するために使用されます。

define i32 @main() {
entry:
  %hdl = call ptr @f(i32 4)
  call void @llvm.coro.resume(ptr %hdl)
  call void @llvm.coro.resume(ptr %hdl)
  call void @llvm.coro.destroy(ptr %hdl)
  ret i32 0
}

コルーチンが実行中のときに存在する関数スタックフレームに加えて、コルーチンが中断されたときにコルーチンの状態を保持するオブジェクトを含む追加のストレージ領域があります。このストレージ領域は、コルーチンフレームと呼ばれます。これは、コルーチンが呼び出されたときに作成され、コルーチンが完了するまで実行されるか、中断された状態で破棄されるときに破棄されます。

LLVM は現在、2 つのスタイルのコルーチンローワーリングをサポートしています。これらのスタイルは、実質的に異なる機能セットをサポートし、実質的に異なる ABI を持ち、実質的に異なるフロントエンドコード生成パターンを想定しています。しかし、これらのスタイルには共通点も多くあります。

すべての場合において、LLVM コルーチンは、最初に、コルーチンの構造を定義するコルーチン組み込み関数への呼び出しを持つ通常の LLVM 関数として表現されます。次に、コルーチン関数は、最も一般的なケースでは、コルーチンローワーリングパスによって、「ランプ関数」に書き換えられます。これはコルーチンの最初の入り口であり、最初にサスペンドポイントに到達するまで実行されます。元のコルーチン関数の残りの部分は、いくつかの「レジューム関数」に分割されます。中断をまたいで永続化する必要がある状態はすべて、コルーチンフレームに格納されます。レジューム関数は、コルーチンの通常の実行を継続する「正常」な再開と、サスペンドを試みることなくコルーチンをアンワインドする必要がある「異常」な再開のいずれかを処理できなければなりません。

スイッチド・レジューム・ローワーリング

LLVM の標準的なスイッチド・レジューム・ローワーリングでは、llvm.coro.id の使用によってシグナルが送られ、コルーチンフレームは、コルーチンの特定呼び出しへのハンドルを表す「コルーチンオブジェクト」の一部として格納されます。すべてのコルーチンオブジェクトは、コルーチンの実装について何も知らなくても特定の機能を使用できるようにする共通の ABI をサポートしています。

  • コルーチンオブジェクトがllvm.coro.doneで完了に達したかどうかを問い合わせることができます。

  • コルーチンオブジェクトがllvm.coro.resumeでまだ完了に達していない場合は、正常に再開できます。

  • コルーチンオブジェクトはllvm.coro.destroyで破棄できます。これにより、コルーチンオブジェクトが無効になります。これは、コルーチンが正常に完了した場合でも、個別に行う必要があります。

  • 特定のサイズとアラインメントを持つことがわかっている「プロミス」ストレージは、llvm.coro.promiseを使用してコルーチンオブジェクトから投影できます。コルーチン実装は、同じサイズとアラインメントのプロミスを定義するようにコンパイルされている必要があります。

一般に、コルーチンオブジェクトが実行中の間にこれらのいずれかの方法でコルーチンオブジェクトと対話すると、未定義の動作になります。

コルーチン関数は、制御がコルーチンに入る 3 つの異なる方法を表す 3 つの関数に分割されます。

  1. 最初に呼び出されるランプ関数。任意の引数を取り、コルーチンオブジェクトへのポインタを返します。

  2. コルーチンが再開されたときに呼び出されるコルーチンレジューム関数。コルーチンオブジェクトへのポインタを取り、voidを返します。

  3. コルーチンが破棄されたときに呼び出されるコルーチン破棄関数。コルーチンオブジェクトへのポインタを取り、voidを返します。

レジューム関数と破棄関数はすべてのサスペンドポイント間で共有されるため、サスペンドポイントはアクティブなサスペンドのインデックスをコルーチンオブジェクトに格納する必要があり、レジューム/破棄関数はそのインデックスを切り替えて正しいポイントに戻る必要があります。したがって、このローワーリングの名前が付けられています。

レジューム関数と破棄関数へのポインタは、すべてのコルーチンに対して固定されている既知のオフセットでコルーチンオブジェクトに格納されます。完了したコルーチンは、ヌルレジューム関数で表されます。

コルーチンオブジェクトを割り当ておよび解放するためのやや複雑な組み込み関数のプロトコルがあります。インライン化のために割り当てを省略できるように、これは複雑です。このプロトコルについては、以下で詳しく説明します。

フロントエンドはコルーチン関数を直接呼び出すコードを生成する場合があります。これはランプ関数への呼び出しになり、コルーチンオブジェクトへのポインタを返します。フロントエンドは、対応する組み込み関数を使用して常にコルーチンを再開または破棄する必要があります。

リターンド・コンティニュエーション・ローワーリング

llvm.coro.id.retcon または llvm.coro.id.retcon.once の使用によってシグナルが送られる、リターンド・コンティニュエーション・ローワーリングでは、ABI の一部の側面をフロントエンドでより明示的に処理する必要があります。

このローワーリングでは、すべてのサスペンドポイントは、継続関数と呼ばれる関数ポインタと一緒に呼び出し元に返される「イールド値」のリストを受け取ります。コルーチンは、この継続関数ポインタを呼び出すだけで再開されます。元のコルーチンは、ランプ関数と、各サスペンドポイントに対して 1 つずつ、これらの継続関数の任意の数に分割されます。

LLVM は実際には、密接に関連する 2 つのリターンド・コンティニュエーション・ローワーリングをサポートしています。

  • 通常のリターンド・コンティニュエーション・ローワーリングでは、コルーチンは複数回自身をサスペンドできます。これは、継続関数自体が、イールド値のリストと同様に、別の継続ポインタを返すことを意味します。

    コルーチンは、ヌルの継続ポインタを返すことによって、完了まで実行されたことを示します。イールドされた値は、undef になり、無視する必要があります。

  • イールド・ワンス・リターンド・コンティニュエーション・ローワーリングでは、コルーチンは正確に 1 回自身をサスペンドする必要があります(または例外をスローします)。ランプ関数は継続関数ポインタとイールド値を返します。継続関数は、コルーチンが完了まで実行されたときに、オプションで通常の結果を返すことができます。

コルーチンフレームは、coro.id組み込み関数に渡される固定サイズのバッファで維持され、静的に特定のサイズとアラインメントが保証されます。同じバッファを継続関数にも渡す必要があります。バッファが不十分な場合、コルーチンはメモリを割り当てます。その場合、少なくともそのポインタをバッファに格納する必要があります。したがって、バッファは常にポインタサイズ以上である必要があります。コルーチンがバッファを使用する方法は、サスペンドポイントによって異なる場合があります。

バッファポインタに加えて、継続関数は、コルーチンが正常に再開されているか(ゼロ)、または異常に再開されているか(非ゼロ)を示す引数を取ります。

LLVM は現在、リターンド・コンティニュエーションコルーチンを呼び出し元に完全にインライン化した後、静的に割り当てを削除することが効果的ではありません。LLVM のコルーチンサポートが主に低レベルのローワーリングに使用され、インライン化がパイプラインのより早い段階で適用されることが予想される場合は、これを受け入れることができます。

非同期ローワーリング

llvm.coro.id.async の使用によってシグナルが送られる非同期継続ローワーリングでは、制御フローの処理をフロントエンドで明示的に処理する必要があります。

このローワーリングでは、コルーチンは現在の非同期コンテキストを引数の 1 つとして受け取ると想定されています(引数の位置はllvm.coro.id.asyncによって決定されます)。コルーチンの引数と戻り値のマージングに使用されます。したがって、非同期コルーチンはvoidを返します。

define swiftcc void @async_coroutine(ptr %async.ctxt, ptr, ptr) {
}

サスペンドポイントをまたいで存続する値は、継続関数で使用できるように、コルーチンフレームに格納する必要があります。このフレームは、非同期コンテキストのテールとして格納されます。

すべてのサスペンドポイントは、継続の非同期コンテキストを取得する方法を記述するコンテキスト射影関数引数を受け取り、すべてのサスペンドポイントには、llvm.coro.async.resume組み込み関数で示される関連付けられたレジューム関数があります。コルーチンは、このレジューム関数を呼び出し、その引数の 1 つとして非同期コンテキストを渡すことで再開されます。レジューム関数は、フロントエンドによってllvm.coro.suspend.async組み込み関数へのパラメータとして提供されるコンテキスト射影関数を適用することにより、その(呼び出し元の)非同期コンテキストを復元できます。

// For example:
struct async_context {
  struct async_context *caller_context;
  ...
}

char *context_projection_function(struct async_context *callee_ctxt) {
   return callee_ctxt->caller_context;
}
%resume_func_ptr = call ptr @llvm.coro.async.resume()
call {ptr, ptr, ptr} (ptr, ptr, ...) @llvm.coro.suspend.async(
                                            ptr %resume_func_ptr,
                                            ptr %context_projection_function

フロントエンドは、llvm.coro.id.asyncの引数によって、各非同期コルーチンに関連付けられた非同期関数ポインタ構造体を提供する必要があります。非同期コンテキストの初期サイズとアラインメントは、llvm.coro.id.async組み込み関数への引数として提供する必要があります。低レベル化処理では、コルーチンフレームの要件に合わせてサイズエントリを更新します。フロントエンドは非同期コンテキストのメモリを割り当てる責任がありますが、必要なサイズを取得するために非同期関数ポインタ構造体を使用できます。

struct async_function_pointer {
  uint32_t relative_function_pointer_to_async_impl;
  uint32_t context_size;
}

低レベル化処理では、非同期コルーチンをランプ関数と、サスペンドポイントごとに1つのレジューム関数に分割します。

呼び出し元、サスペンドポイント、レジューム関数への制御フローの受け渡し方法は、フロントエンドに委ねられます。

サスペンドポイントは、関数とその引数を受け取ります。この関数は、呼び出し先関数への転送をモデル化することを目的としています。低レベル化処理によってテールコールされるため、非同期コルーチンと同じシグネチャと呼び出し規約を持つ必要があります。

call {ptr, ptr, ptr} (ptr, ptr, ...) @llvm.coro.suspend.async(
                 ptr %resume_func_ptr,
                 ptr %context_projection_function,
                 ptr %suspend_function,
                 ptr %arg1, ptr %arg2, i8 %arg3)

コルーチン例

以下の例はすべて、スイッチド・レジューム・コルーチンです。

コルーチン表現

次の擬似コードでスケッチされた動作を持つLLVMコルーチンの例を見てみましょう。

void *f(int n) {
   for(;;) {
     print(n++);
     <suspend> // returns a coroutine handle on first suspend
   }
}

このコルーチンは、引数として値nを使用して関数printを呼び出し、実行を中断します。このコルーチンが再開されるたびに、前回よりも1つ大きい引数を使用して再度printを呼び出します。このコルーチンは単独で完了することはなく、明示的に破棄する必要があります。前のセクションで示したmainでこのコルーチンを使用すると、値4、5、6でprintを呼び出し、その後コルーチンは破棄されます。

このコルーチンのLLVM IRは次のようになります。

define ptr @f(i32 %n) presplitcoroutine {
entry:
  %id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
  %size = call i32 @llvm.coro.size.i32()
  %alloc = call ptr @malloc(i32 %size)
  %hdl = call noalias ptr @llvm.coro.begin(token %id, ptr %alloc)
  br label %loop
loop:
  %n.val = phi i32 [ %n, %entry ], [ %inc, %loop ]
  %inc = add nsw i32 %n.val, 1
  call void @print(i32 %n.val)
  %0 = call i8 @llvm.coro.suspend(token none, i1 false)
  switch i8 %0, label %suspend [i8 0, label %loop
                                i8 1, label %cleanup]
cleanup:
  %mem = call ptr @llvm.coro.free(token %id, ptr %hdl)
  call void @free(ptr %mem)
  br label %suspend
suspend:
  %unused = call i1 @llvm.coro.end(ptr %hdl, i1 false, token none)
  ret ptr %hdl
}

entryブロックは、コルーチンフレームを確立します。coro.size組み込み関数は、コルーチンフレームに必要なサイズを表す定数に低レベル化されます。coro.begin組み込み関数は、コルーチンフレームを初期化し、コルーチンハンドルを返します。coro.beginの2番目のパラメータは、コルーチンフレームを動的に割り当てる必要がある場合に使用するメモリブロックが与えられます。coro.id組み込み関数は、ジャンプスレッディングなどの最適化パスによってcoro.begin組み込み関数が重複する場合に役立つコルーチンIDとして機能します。

cleanupブロックは、コルーチンフレームを破棄します。coro.free組み込み関数は、コルーチンハンドルが与えられると、解放するメモリブロックへのポインタを返すか、コルーチンフレームが動的に割り当てられなかった場合はnullを返します。cleanupブロックは、コルーチンが単独で完了した場合、またはcoro.destroy組み込み関数への呼び出しによって破棄された場合に入ります。

suspendブロックには、コルーチンが完了したとき、または中断されたときに実行されるコードが含まれています。coro.end組み込み関数は、コルーチンの初期呼び出しではない場合、コルーチンが呼び出し元に制御を返す必要があるポイントをマークします。

loopブロックは、コルーチンの本体を表します。coro.suspend組み込み関数は、次のスイッチと組み合わせて、コルーチンが中断された(デフォルトケース)、再開された(ケース0)、または破棄された(ケース1)ときに制御フローがどうなるかを示します。

コルーチン変換

コルーチン低レベル化のステップの1つは、コルーチンフレームの構築です。定義・使用チェーンを分析して、どのオブジェクトをサスペンドポイントを越えて存続させる必要があるかを判断します。前のセクションで示したコルーチンでは、仮想レジスタ%incの使用は、サスペンドポイントによって定義から分離されているため、コルーチンが中断されて制御が呼び出し元に戻ると、コルーチンフレームが消えるため、スタックフレームに常駐できません。i32スロットがコルーチンフレームに割り当てられ、%incは必要に応じてそのスロットからスピルされ、再ロードされます。

また、レジューム関数と破棄関数のアドレスも格納して、coro.resumeおよびcoro.destroy組み込み関数が、コンパイル時にそのIDを静的に特定できない場合にコルーチンを再開および破棄できるようにします。例として、コルーチンフレームは次のようになります。

%f.frame = type { ptr, ptr, i32 }

レジュームと破棄部分がアウトライン化された後、関数fには、コルーチンフレームの作成と初期化、およびサスペンドポイントに到達するまでのコルーチンの実行を担当するコードのみが含まれます。

define ptr @f(i32 %n) {
entry:
  %id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
  %alloc = call noalias ptr @malloc(i32 24)
  %frame = call noalias ptr @llvm.coro.begin(token %id, ptr %alloc)
  %1 = getelementptr %f.frame, ptr %frame, i32 0, i32 0
  store ptr @f.resume, ptr %1
  %2 = getelementptr %f.frame, ptr %frame, i32 0, i32 1
  store ptr @f.destroy, ptr %2

  %inc = add nsw i32 %n, 1
  %inc.spill.addr = getelementptr inbounds %f.Frame, ptr %FramePtr, i32 0, i32 2
  store i32 %inc, ptr %inc.spill.addr
  call void @print(i32 %n)

  ret ptr %frame
}

コルーチンのアウトライン化されたレジューム部分は、関数f.resumeに存在します。

define internal fastcc void @f.resume(ptr %frame.ptr.resume) {
entry:
  %inc.spill.addr = getelementptr %f.frame, ptr %frame.ptr.resume, i64 0, i32 2
  %inc.spill = load i32, ptr %inc.spill.addr, align 4
  %inc = add i32 %inc.spill, 1
  store i32 %inc, ptr %inc.spill.addr, align 4
  tail call void @print(i32 %inc)
  ret void
}

一方、関数f.destroyにはコルーチンのクリーンアップコードが含まれます。

define internal fastcc void @f.destroy(ptr %frame.ptr.destroy) {
entry:
  tail call void @free(ptr %frame.ptr.destroy)
  ret void
}

ヒープ割り当ての回避

コルーチンが作成され、操作され、同じ呼び出し関数によって破棄される、概要セクションのmain関数で説明されている特定のコルーチン使用パターンは、RAIIイディオムを実装するコルーチンでは一般的であり、呼び出し元に静的なallocaとしてコルーチンフレームを格納することで動的割り当てを回避する割り当て省略最適化に適しています。

エントリブロックでは、動的割り当てが必要な場合はtrueを返し、動的割り当てが省略された場合はfalseを返すcoro.alloc組み込み関数を呼び出します。

entry:
  %id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
  %need.dyn.alloc = call i1 @llvm.coro.alloc(token %id)
  br i1 %need.dyn.alloc, label %dyn.alloc, label %coro.begin
dyn.alloc:
  %size = call i32 @llvm.coro.size.i32()
  %alloc = call ptr @CustomAlloc(i32 %size)
  br label %coro.begin
coro.begin:
  %phi = phi ptr [ null, %entry ], [ %alloc, %dyn.alloc ]
  %hdl = call noalias ptr @llvm.coro.begin(token %id, ptr %phi)

クリーンアップブロックでは、coro.free組み込み関数を条件としてコルーチンフレームの解放を行います。割り当てが省略されている場合、coro.freenullを返し、したがって割り当て解除コードをスキップします。

cleanup:
  %mem = call ptr @llvm.coro.free(token %id, ptr %hdl)
  %need.dyn.free = icmp ne ptr %mem, null
  br i1 %need.dyn.free, label %dyn.free, label %if.end
dyn.free:
  call void @CustomFree(ptr %mem)
  br label %if.end
if.end:
  ...

上記のように表現された割り当てと割り当て解除を使用して、コルーチンヒープ割り当て省略最適化の後、結果のmainは次のようになります。

define i32 @main() {
entry:
  call void @print(i32 4)
  call void @print(i32 5)
  call void @print(i32 6)
  ret i32 0
}

複数のサスペンドポイント

複数のサスペンドポイントを持つコルーチンを検討してみましょう。

void *f(int n) {
   for(;;) {
     print(n++);
     <suspend>
     print(-n);
     <suspend>
   }
}

一致するLLVMコードは次のようになります(残りのコードは前のセクションのコードと同じままです)。

loop:
  %n.addr = phi i32 [ %n, %entry ], [ %inc, %loop.resume ]
  call void @print(i32 %n.addr) #4
  %2 = call i8 @llvm.coro.suspend(token none, i1 false)
  switch i8 %2, label %suspend [i8 0, label %loop.resume
                                i8 1, label %cleanup]
loop.resume:
  %inc = add nsw i32 %n.addr, 1
  %sub = xor i32 %n.addr, -1
  call void @print(i32 %sub)
  %3 = call i8 @llvm.coro.suspend(token none, i1 false)
  switch i8 %3, label %suspend [i8 0, label %loop
                                i8 1, label %cleanup]

この場合、コルーチンフレームには、コルーチンを再開する必要があるサスペンドポイントを示すサスペンドインデックスが含まれます。

%f.frame = type { ptr, ptr, i32, i32 }

レジューム関数は、インデックスを使用して適切な基本ブロックにジャンプし、次のようになります。

define internal fastcc void @f.Resume(ptr %FramePtr) {
entry.Resume:
  %index.addr = getelementptr inbounds %f.Frame, ptr %FramePtr, i64 0, i32 2
  %index = load i8, ptr %index.addr, align 1
  %switch = icmp eq i8 %index, 0
  %n.addr = getelementptr inbounds %f.Frame, ptr %FramePtr, i64 0, i32 3
  %n = load i32, ptr %n.addr, align 4

  br i1 %switch, label %loop.resume, label %loop

loop.resume:
  %sub = sub nsw i32 0, %n
  call void @print(i32 %sub)
  br label %suspend
loop:
  %inc = add nsw i32 %n, 1
  store i32 %inc, ptr %n.addr, align 4
  tail call void @print(i32 %inc)
  br label %suspend

suspend:
  %storemerge = phi i8 [ 0, %loop ], [ 1, %loop.resume ]
  store i8 %storemerge, ptr %index.addr, align 1
  ret void
}

異なるサスペンドポイントに対して異なるクリーンアップコードを実行する必要がある場合、同様のスイッチがf.destroy関数にあります。

注記

コルーチン状態にサスペンドインデックスを使用し、f.resumeおよびf.destroyにスイッチを使用することは、可能な実装戦略の1つです。サスペンドポイントごとに異なるf.resume1f.resume2などが作成され、インデックスを格納する代わりに、レジューム関数と破棄関数ポインタがすべてのサスペンドで更新される別のオプションを検討しました。初期テストでは、現在の方法が後者よりもオプティマイザーに優しいことが示されたため、現時点で実装されている低レベル化戦略です。

個別の保存と中断

前の例では、レジュームインデックスの設定(またはコルーチンを再開する準備のために発生する必要がある他の状態変更)は、コルーチンのサスペンドと同時に発生します。ただし、場合によっては、コルーチンが再開の準備が整うタイミングと、コルーチンが中断されるタイミングを制御する必要があります。

次の例では、コルーチンは、非同期操作async_op1async_op2の完了によって駆動されるアクティビティを表します。これらは、パラメータとしてコルーチンハンドルを取得し、非同期操作が完了するとコルーチンを再開します。

void g() {
   for (;;)
     if (cond()) {
        async_op1(<coroutine-handle>); // will resume once async_op1 completes
        <suspend>
        do_one();
     }
     else {
        async_op2(<coroutine-handle>); // will resume once async_op2 completes
        <suspend>
        do_two();
     }
   }
}

この場合、コルーチンはasync_op1およびasync_op2の呼び出しの前に再開する準備が整っている必要があります。coro.save組み込み関数は、コルーチンが再開の準備を整える必要のあるポイント(つまり、正しい再開ポイントで再開できるように、レジュームインデックスがコルーチンフレームに格納される必要がある場合)を示すために使用されます。

if.true:
  %save1 = call token @llvm.coro.save(ptr %hdl)
  call void @async_op1(ptr %hdl)
  %suspend1 = call i1 @llvm.coro.suspend(token %save1, i1 false)
  switch i8 %suspend1, label %suspend [i8 0, label %resume1
                                       i8 1, label %cleanup]
if.false:
  %save2 = call token @llvm.coro.save(ptr %hdl)
  call void @async_op2(ptr %hdl)
  %suspend2 = call i1 @llvm.coro.suspend(token %save2, i1 false)
  switch i8 %suspend2, label %suspend [i8 0, label %resume2
                                       i8 1, label %cleanup]

コルーチンプラミス

コルーチン作成者またはフロントエンドは、コルーチンとの通信に使用できる区別されたallocaを指定できます。この区別されたallocaは、コルーチンプラミスと呼ばれ、coro.id組み込み関数の2番目のパラメータとして提供されます。

次のコルーチンは、32ビット整数promiseを指定し、それを使用してコルーチンによって生成された現在の値を格納します。

define ptr @f(i32 %n) {
entry:
  %promise = alloca i32
  %id = call token @llvm.coro.id(i32 0, ptr %promise, ptr null, ptr null)
  %need.dyn.alloc = call i1 @llvm.coro.alloc(token %id)
  br i1 %need.dyn.alloc, label %dyn.alloc, label %coro.begin
dyn.alloc:
  %size = call i32 @llvm.coro.size.i32()
  %alloc = call ptr @malloc(i32 %size)
  br label %coro.begin
coro.begin:
  %phi = phi ptr [ null, %entry ], [ %alloc, %dyn.alloc ]
  %hdl = call noalias ptr @llvm.coro.begin(token %id, ptr %phi)
  br label %loop
loop:
  %n.val = phi i32 [ %n, %coro.begin ], [ %inc, %loop ]
  %inc = add nsw i32 %n.val, 1
  store i32 %n.val, ptr %promise
  %0 = call i8 @llvm.coro.suspend(token none, i1 false)
  switch i8 %0, label %suspend [i8 0, label %loop
                                i8 1, label %cleanup]
cleanup:
  %mem = call ptr @llvm.coro.free(token %id, ptr %hdl)
  call void @free(ptr %mem)
  br label %suspend
suspend:
  %unused = call i1 @llvm.coro.end(ptr %hdl, i1 false, token none)
  ret ptr %hdl
}

コルーチンコンシューマーは、coro.promise組み込み関数を使用して、コルーチンプラミスにアクセスできます。

define i32 @main() {
entry:
  %hdl = call ptr @f(i32 4)
  %promise.addr = call ptr @llvm.coro.promise(ptr %hdl, i32 4, i1 false)
  %val0 = load i32, ptr %promise.addr
  call void @print(i32 %val0)
  call void @llvm.coro.resume(ptr %hdl)
  %val1 = load i32, ptr %promise.addr
  call void @print(i32 %val1)
  call void @llvm.coro.resume(ptr %hdl)
  %val2 = load i32, ptr %promise.addr
  call void @print(i32 %val2)
  call void @llvm.coro.destroy(ptr %hdl)
  ret i32 0
}

このセクションの例をコンパイルすると、コンパイルの結果は次のようになります。

define i32 @main() {
entry:
  tail call void @print(i32 4)
  tail call void @print(i32 5)
  tail call void @print(i32 6)
  ret i32 0
}

最終中断

コルーチン作成者またはフロントエンドは、coro.suspend組み込み関数の2番目の引数をtrueに設定することにより、特定の中断を最終中断として指定できます。このようなサスペンドポイントには2つのプロパティがあります。

  • coro.done組み込み関数を介して、中断されたコルーチンが最終中断ポイントにあるかどうかを確認できます。

  • 最終中断ポイントで停止したコルーチンを再開すると、未定義の動作になります。最終中断ポイントにあるコルーチンで可能な唯一のアクションは、coro.destroy組み込み関数を使用して破棄することです。

ユーザーの観点から見ると、最終中断ポイントは、コルーチンが終わりに達するという概念を表しています。コンパイラの観点から見ると、レジューム関数のレジュームポイント(したがってスイッチケース)の数を減らすための最適化の機会です。

以下は、最終中断ポイントに到達するまでコルーチンを再開し続け、その後コルーチンが破棄される関数の例です。

define i32 @main() {
entry:
  %hdl = call ptr @f(i32 4)
  br label %while
while:
  call void @llvm.coro.resume(ptr %hdl)
  %done = call i1 @llvm.coro.done(ptr %hdl)
  br i1 %done, label %end, label %while
end:
  call void @llvm.coro.destroy(ptr %hdl)
  ret i32 0
}

通常、最終中断ポイントは、高レベル言語の明示的に作成された中断ポイントに対応しないフロントエンドインジェクション中断ポイントです。たとえば、中断ポイントが1つしかないPythonジェネレーターの場合

def coroutine(n):
  for i in range(n):
    yield i

Pythonフロントエンドはさらに2つの中断ポイントを挿入して、実際のコードが次のようになるようにします。

void* coroutine(int n) {
  int current_value;
  <designate current_value to be coroutine promise>
  <SUSPEND> // injected suspend point, so that the coroutine starts suspended
  for (int i = 0; i < n; ++i) {
    current_value = i; <SUSPEND>; // corresponds to "yield i"
  }
  <SUSPEND final=true> // injected final suspend point
}

Pythonイテレータ__next__は次のようになります。

int __next__(void* hdl) {
  coro.resume(hdl);
  if (coro.done(hdl)) throw StopIteration();
  return *(int*)coro.promise(hdl, 4, false);
}

組み込み関数

コルーチン操作組み込み関数

このセクションで説明する組み込み関数は、既存のコルーチンを操作するために使用されます。これらは、コルーチンフレームへのポインタまたはコルーチンプラミスへのポインタを持つ任意の関数で使用できます。

‘llvm.coro.destroy’組み込み関数

構文:
declare void @llvm.coro.destroy(ptr <handle>)
概要:

llvm.coro.destroy’ 組み込み関数は、中断されたスイッチ再開コルーチンを破棄します。

引数:

引数は、中断されたコルーチンへのコルーチンハンドルです。

セマンティクス:

可能な場合、coro.destroy 組み込み関数はコルーチン破棄関数への直接呼び出しに置き換えられます。それ以外の場合は、コルーチンフレームに格納されている破棄関数の関数ポインターに基づいて間接呼び出しに置き換えられます。中断されていないコルーチンを破棄すると、未定義の動作になります。

‘llvm.coro.resume’ 組み込み関数

declare void @llvm.coro.resume(ptr <handle>)
概要:

llvm.coro.resume’ 組み込み関数は、中断されたスイッチ再開コルーチンを再開します。

引数:

引数は、中断されたコルーチンへのハンドルです。

セマンティクス:

可能な場合、coro.resume 組み込み関数はコルーチン再開関数への直接呼び出しに置き換えられます。それ以外の場合は、コルーチンフレームに格納されている再開関数の関数ポインターに基づいて間接呼び出しに置き換えられます。中断されていないコルーチンを再開すると、未定義の動作になります。

‘llvm.coro.done’ 組み込み関数

declare i1 @llvm.coro.done(ptr <handle>)
概要:

llvm.coro.done’ 組み込み関数は、中断されたスイッチ再開コルーチンが最後のサスペンドポイントにあるかどうかを確認します。

引数:

引数は、中断されたコルーチンへのハンドルです。

セマンティクス:

この組み込み関数を、最後のサスペンドポイントがないコルーチンや中断されていないコルーチンで使用すると、未定義の動作になります。

‘llvm.coro.promise’ 組み込み関数

declare ptr @llvm.coro.promise(ptr <ptr>, i32 <alignment>, i1 <from>)
概要:

llvm.coro.promise’ 組み込み関数は、スイッチ再開コルーチンハンドルが与えられたときに、コルーチンプロミスへのポインターを取得し、その逆も行います。

引数:

最初の引数は、from が false の場合はコルーチンへのハンドルです。それ以外の場合は、コルーチンプロミスへのポインターです。

2 番目の引数は、プロミスの配置要件です。フロントエンドが %promise = alloca i32 をプロミスとして指定した場合、coro.promise への配置引数は、ターゲットプラットフォーム上の i32 の配置である必要があります。フロントエンドが %promise = alloca i32, align 16 をプロミスとして指定した場合、配置引数は 16 である必要があります。この引数は定数のみを受け入れます。

3 番目の引数は、変換の方向を示すブール値です。from が true の場合、組み込み関数はプロミスへのポインターが与えられたときにコルーチンハンドルを返します。from が false の場合、組み込み関数はコルーチンハンドルからプロミスへのポインターを返します。この引数は定数のみを受け入れます。

セマンティクス:

この組み込み関数をコルーチンプロミスを持たないコルーチンで使用すると、未定義の動作になります。現在実行中のコルーチンのコルーチンプロミスを読み取りおよび変更することができます。コルーチン作成者とコルーチンユーザーは、データ競合がないことを確認する責任があります。

例:
define ptr @f(i32 %n) {
entry:
  %promise = alloca i32
  ; the second argument to coro.id points to the coroutine promise.
  %id = call token @llvm.coro.id(i32 0, ptr %promise, ptr null, ptr null)
  ...
  %hdl = call noalias ptr @llvm.coro.begin(token %id, ptr %alloc)
  ...
  store i32 42, ptr %promise ; store something into the promise
  ...
  ret ptr %hdl
}

define i32 @main() {
entry:
  %hdl = call ptr @f(i32 4) ; starts the coroutine and returns its handle
  %promise.addr = call ptr @llvm.coro.promise(ptr %hdl, i32 4, i1 false)
  %val = load i32, ptr %promise.addr ; load a value from the promise
  call void @print(i32 %val)
  call void @llvm.coro.destroy(ptr %hdl)
  ret i32 0
}

コルーチン構造組み込み関数

このセクションで説明されている組み込み関数は、コルーチン内でコルーチン構造を記述するために使用されます。コルーチン外で使用しないでください。

‘llvm.coro.size’ 組み込み関数

declare i32 @llvm.coro.size.i32()
declare i64 @llvm.coro.size.i64()
概要:

llvm.coro.size’ 組み込み関数は、コルーチンフレームを格納するために必要なバイト数を返します。これは、スイッチ再開コルーチンでのみサポートされています。

引数:

なし

セマンティクス:

coro.size 組み込み関数は、コルーチンフレームのサイズを表す定数に変換されます。

‘llvm.coro.align’ 組み込み関数

declare i32 @llvm.coro.align.i32()
declare i64 @llvm.coro.align.i64()
概要:

llvm.coro.align’ 組み込み関数は、コルーチンフレームの配置を返します。これは、スイッチ再開コルーチンでのみサポートされています。

引数:

なし

セマンティクス:

coro.align 組み込み関数は、コルーチンフレームの配置を表す定数に変換されます。

‘llvm.coro.begin’ 組み込み関数

declare ptr @llvm.coro.begin(token <id>, ptr <mem>)
概要:

llvm.coro.begin’ 組み込み関数は、コルーチンフレームのアドレスを返します。

引数:

最初の引数は、コルーチンを識別する ‘llvm.coro.id’ の呼び出しによって返されたトークンです。

2 番目の引数は、コルーチンフレームが動的に割り当てられる場合に格納されるメモリブロックへのポインターです。このポインターは、返された継続コルーチンでは無視されます。

セマンティクス:

コルーチンフレーム内のオブジェクトの配置要件や、コード生成の簡潔さの理由に応じて、coro.begin から返されるポインターは、%mem 引数からのオフセットにある場合があります。(これは、データへの相対的なアクセスを表す命令が、小さく正および負のオフセットでよりコンパクトにエンコードできる場合に役立つ可能性があります)。

フロントエンドは、コルーチンごとに 1 つの coro.begin 組み込み関数を正確に発行する必要があります。

‘llvm.coro.free’ 組み込み関数

declare ptr @llvm.coro.free(token %id, ptr <frame>)
概要:

llvm.coro.free’ 組み込み関数は、コルーチンフレームが格納されているメモリブロックへのポインターを返すか、このコルーチンのインスタンスがコルーチンフレームに動的に割り当てられたメモリを使用していない場合は null を返します。この組み込み関数は、返された継続コルーチンではサポートされていません。

引数:

最初の引数は、コルーチンを識別する ‘llvm.coro.id’ の呼び出しによって返されたトークンです。

2 番目の引数は、コルーチンフレームへのポインターです。これは、以前の coro.begin 呼び出しによって返されたのと同じポインターである必要があります。

例(カスタム割り当て解除関数):
cleanup:
  %mem = call ptr @llvm.coro.free(token %id, ptr %frame)
  %mem_not_null = icmp ne ptr %mem, null
  br i1 %mem_not_null, label %if.then, label %if.end
if.then:
  call void @CustomFree(ptr %mem)
  br label %if.end
if.end:
  ret void
例(標準の割り当て解除関数):
cleanup:
  %mem = call ptr @llvm.coro.free(token %id, ptr %frame)
  call void @free(ptr %mem)
  ret void

‘llvm.coro.alloc’ 組み込み関数

declare i1 @llvm.coro.alloc(token <id>)
概要:

llvm.coro.alloc’ 組み込み関数は、コルーチンフレーム用のメモリを取得するために動的割り当てが必要な場合は true を、それ以外の場合は false を返します。これは、返された継続コルーチンではサポートされていません。

引数:

最初の引数は、コルーチンを識別する ‘llvm.coro.id’ の呼び出しによって返されたトークンです。

セマンティクス:

フロントエンドは、コルーチンごとに最大 1 つの coro.alloc 組み込み関数を発行する必要があります。この組み込み関数は、可能な場合にコルーチンフレームの動的割り当てを抑制するために使用されます。

例:
entry:
  %id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
  %dyn.alloc.required = call i1 @llvm.coro.alloc(token %id)
  br i1 %dyn.alloc.required, label %coro.alloc, label %coro.begin

coro.alloc:
  %frame.size = call i32 @llvm.coro.size()
  %alloc = call ptr @MyAlloc(i32 %frame.size)
  br label %coro.begin

coro.begin:
  %phi = phi ptr [ null, %entry ], [ %alloc, %coro.alloc ]
  %frame = call ptr @llvm.coro.begin(token %id, ptr %phi)

‘llvm.coro.noop’ 組み込み関数

declare ptr @llvm.coro.noop()
概要:

llvm.coro.noop’ 組み込み関数は、再開または破棄されたときに何もしないコルーチンのコルーチンフレームのアドレスを返します。

引数:

なし

セマンティクス:

この組み込み関数は、プライベート定数コルーチンフレームを参照するように変換されます。このフレームの再開および破棄ハンドラーは、何も行わない空の関数です。異なる翻訳単位では、llvm.coro.noop が異なるポインターを返す可能性があることに注意してください。

‘llvm.coro.frame’ 組み込み関数

declare ptr @llvm.coro.frame()
概要:

llvm.coro.frame’ 組み込み関数は、囲んでいるコルーチンのコルーチンフレームのアドレスを返します。

引数:

なし

セマンティクス:

この組み込み関数は、coro.begin 命令を参照するように変換されます。これは、コルーチンフレームを参照しやすくするためのフロントエンドの便宜的な組み込み関数です。

‘llvm.coro.id’ 組み込み関数

declare token @llvm.coro.id(i32 <align>, ptr <promise>, ptr <coroaddr>,
                                                        ptr <fnaddrs>)
概要:

llvm.coro.id’ 組み込み関数は、スイッチ再開コルーチンを識別するトークンを返します。

引数:

最初の引数は、割り当て関数によって返され、最初の引数によって coro.begin に与えられるメモリの配置に関する情報を提供します。この引数が 0 の場合、メモリは 2 * sizeof(ptr) に配置されていると見なされます。この引数は定数のみを受け入れます。

2 番目の引数が null でない場合、コルーチンプロミスである特定の alloca 命令を指定します。

3 番目の引数は、フロントエンドから出てくる null です。CoroEarly パスは、この引数をこの coro.id が属する関数を指すように設定します。

4番目の引数は、コルーチンが分割される前はnullであり、後でコルーチンのアウトライン化された再開部分と破棄部分への関数ポインタを含むプライベートなグローバル定数配列を指すように置き換えられます。

意味:

この組み込み関数の目的は、同じコルーチンに属するcoro.idcoro.alloccoro.beginを関連付け、コルーチン全体が複製されない限り、最適化パスがこれらの命令のいずれも複製しないようにすることです。

フロントエンドは、コルーチンごとに正確に1つのcoro.id組み込み関数を発行する必要があります。

フロントエンドは、コルーチンに対して関数属性presplitcoroutineを発行する必要があります。

‘llvm.coro.id.async’ 組み込み関数

declare token @llvm.coro.id.async(i32 <context size>, i32 <align>,
                                  ptr <context arg>,
                                  ptr <async function pointer>)
概要:

llvm.coro.id.async’ 組み込み関数は、非同期コルーチンを識別するトークンを返します。

引数:

最初の引数は、フロントエンドから要求されたasync contextの初期サイズを提供します。低レイヤーでは、このサイズにフレームストレージに必要なサイズが追加され、その値がasync function pointerに格納されます。

2番目の引数は、async contextのメモリアライメントの保証です。フロントエンドは、メモリがこの値でアライメントされることを保証します。

3番目の引数は、現在のコルーチン内のasync context引数です。

4番目の引数は、async function pointer構造体のアドレスです。低レイヤーでは、この構造体のコンテキストサイズ要件が、この組み込み関数の最初の引数で指定された初期サイズ要件にコルーチンフレームサイズ要件を追加することによって更新されます。

意味:

フロントエンドは、コルーチンごとに正確に1つのcoro.id.async組み込み関数を発行する必要があります。

フロントエンドは、コルーチンに対して関数属性presplitcoroutineを発行する必要があります。

‘llvm.coro.id.retcon’ 組み込み関数

declare token @llvm.coro.id.retcon(i32 <size>, i32 <align>, ptr <buffer>,
                                   ptr <continuation prototype>,
                                   ptr <alloc>, ptr <dealloc>)
概要:

llvm.coro.id.retcon’ 組み込み関数は、複数の一時停止を持つ返却継続コルーチンを識別するトークンを返します。

コルーチンの「結果型シーケンス」は、次のように定義されます。

  • コルーチン関数の戻り値の型がvoidの場合、それは空のシーケンスです。

  • コルーチン関数の戻り値の型がstructの場合、それはそのstructの要素型を順に並べたものです。

  • それ以外の場合は、コルーチン関数の戻り値の型です。

結果型シーケンスの最初の要素はポインタ型でなければなりません。継続関数はこの型に強制的に変換されます。シーケンスの残りは「yield型」であり、コルーチン内の一時停止はこれらの型の引数を取る必要があります。

引数:

最初と2番目の引数は、3番目の引数として提供されるバッファの予想サイズとアライメントです。これらは定数でなければなりません。

4番目の引数は、「継続プロトタイプ関数」と呼ばれるグローバル関数への参照でなければなりません。継続関数の型、呼び出し規約、および属性は、この宣言から取得されます。プロトタイプ関数の戻り値の型は、現在の関数の戻り値の型と一致する必要があります。最初のパラメータ型はポインタ型でなければなりません。2番目のパラメータ型は整数型でなければなりません。これはブールフラグとしてのみ使用されます。

5番目の引数は、メモリの割り当てに使用されるグローバル関数への参照でなければなりません。これは、nullを返したり、例外をスローしたりすることによって失敗してはなりません。整数を取り、ポインタを返す必要があります。

6番目の引数は、メモリの解放に使用されるグローバル関数への参照でなければなりません。ポインタを取り、voidを返す必要があります。

意味:

フロントエンドは、コルーチンに対して関数属性presplitcoroutineを発行する必要があります。

‘llvm.coro.id.retcon.once’ 組み込み関数

declare token @llvm.coro.id.retcon.once(i32 <size>, i32 <align>, ptr <buffer>,
                                        ptr <prototype>,
                                        ptr <alloc>, ptr <dealloc>)
概要:

llvm.coro.id.retcon.once’ 組み込み関数は、一意の一時停止を持つ返却継続コルーチンを識別するトークンを返します。

引数:

llvm.core.id.retconの場合と同様ですが、継続プロトタイプの戻り値の型は、コルーチンの戻り値の型と一致するのではなく、継続の通常の戻り値の型を表す必要があります。

意味:

フロントエンドは、コルーチンに対して関数属性presplitcoroutineを発行する必要があります。

‘llvm.coro.end’ 組み込み関数

declare i1 @llvm.coro.end(ptr <handle>, i1 <unwind>, token <result.token>)
概要:

llvm.coro.end’ は、コルーチンの再開部分の実行が終了し、制御が呼び出し元に戻るべきポイントを示します。

引数:

最初の引数は、囲んでいるコルーチンのコルーチンハンドルを参照する必要があります。フロントエンドは、最初のパラメータとしてnullを提供できます。この場合、coro-earlyパスは、nullを適切なコルーチンハンドル値に置き換えます。

2番目の引数は、このcoro.endが例外のためにコルーチン本体を離れるアンワインドシーケンスの一部であるブロックにある場合はtrue、それ以外の場合はfalseである必要があります。

非自明(非none)のトークン引数は、‘llvm.coro.end.results’ 組み込み関数によって生成されたトークン値である必要のある、一意の一時停止を持つ返却継続コルーチンに対してのみ指定できます。

アンワインドセクションのcoro.end呼び出しでは、noneトークンのみが許可されます

意味:

この組み込み関数の目的は、フロントエンドがコルーチンの最初の呼び出し中にのみ関連し、再開部分と破棄部分には存在すべきではないクリーンアップおよびその他のコードをマークできるようにすることです。

返却継続低レイヤーでは、llvm.coro.endはコルーチンフレームを完全に破棄します。2番目の引数がfalseの場合、null継続ポインタを使用してコルーチンから戻り、次の命令は到達不能になります。2番目の引数がtrueの場合、後続のロジックがアンワインドを再開できるようにフォールスルーします。yield-onceコルーチンでは、最初にllvm.coro.suspend.retconに到達せずに非アンワインドllvm.coro.endに到達すると、未定義の動作になります。

このセクションの残りの部分では、スイッチ式再開低レイヤーでの動作について説明します。

この組み込み関数は、コルーチンが開始、再開、破棄部分に分割されるときに低レイヤー化されます。開始部分では、これはノーオペレーションです。再開および破棄部分では、ret void命令に置き換えられ、coro.end命令を含む残りのブロックは破棄されます。ランディングパッドでは、呼び出し元にアンワインドする適切な命令に置き換えられます。coro.endの処理は、ターゲットがランディングパッドまたはWinEH例外モデルを使用しているかどうかによって異なります。

ランディングパッドベースの例外モデルの場合、フロントエンドは、coro.end組み込み関数を次のように使用することが予想されます。

ehcleanup:
  %InResumePart = call i1 @llvm.coro.end(ptr null, i1 true, token none)
  br i1 %InResumePart, label %eh.resume, label %cleanup.cont

cleanup.cont:
  ; rest of the cleanup

eh.resume:
  %exn = load ptr, ptr %exn.slot, align 8
  %sel = load i32, ptr %ehselector.slot, align 4
  %lpad.val = insertvalue { ptr, i32 } undef, ptr %exn, 0
  %lpad.val29 = insertvalue { ptr, i32 } %lpad.val, i32 %sel, 1
  resume { ptr, i32 } %lpad.val29

CoroSpitパスは、再開関数でcoro.endTrueに置き換え、その結果、呼び出し元への即時アンワインドが発生します。一方、開始関数ではFalseに置き換えられ、コルーチンの最初の呼び出し中にのみ必要な残りのクリーンアップコードに進むことができます。

Windows例外処理モデルの場合、フロントエンドは、次のように囲んでいるcleanup padを参照するfuncletバンドルをアタッチする必要があります。

ehcleanup:
  %tok = cleanuppad within none []
  %unused = call i1 @llvm.coro.end(ptr null, i1 true, token none) [ "funclet"(token %tok) ]
  cleanupret from %tok unwind label %RestOfTheCleanup

CoroSplitパスは、funcletバンドルが存在する場合、coro.end組み込み関数の前にcleanupret from %tok unwind to callerを挿入し、残りのブロックを削除します。

アンワインドパス(引数がtrueの場合)では、coro.endはコルーチンが完了したことをマークし、コルーチンを再度再開すると未定義の動作になり、llvm.coro.donetrueを返すようになります。コルーチンは最後のサスペンドによってすでに完了としてマークされるため、これは通常のパスでは必要ありません。

次の表は、coro.end組み込み関数の処理をまとめたものです。

開始関数内

再開/破棄関数内

unwind=false

なし

ret void

unwind=true

WinEH

コルーチンを完了としてマーク

cleanupret unwind to caller
コルーチンを完了としてマーク

ランディングパッド

コルーチンを完了としてマーク

コルーチンを完了としてマーク

‘llvm.coro.end.results’ 組み込み関数

declare token @llvm.coro.end.results(...)
概要:

llvm.coro.end.results’ 組み込み関数は、一意の一時停止を持つ返却継続コルーチンから返される値をキャプチャします。

引数:

引数の数は、継続関数の戻り値の型と一致する必要があります。

  • 継続関数の戻り値の型がvoidの場合、引数があってはなりません

  • 継続関数の戻り値の型がstructの場合、引数はそのstructの要素型を順に並べたものになります。

  • それ以外の場合は、継続関数の戻り値です。

define {ptr, ptr} @g(ptr %buffer, ptr %ptr, i8 %val) presplitcoroutine {
entry:
  %id = call token @llvm.coro.id.retcon.once(i32 8, i32 8, ptr %buffer,
                                             ptr @prototype,
                                             ptr @allocate, ptr @deallocate)
  %hdl = call ptr @llvm.coro.begin(token %id, ptr null)

...

cleanup:
  %tok = call token (...) @llvm.coro.end.results(i8 %val)
  call i1 @llvm.coro.end(ptr %hdl, i1 0, token %tok)
  unreachable

...

declare i8 @prototype(ptr, i1 zeroext)

‘llvm.coro.end.async’ 組み込み関数

declare i1 @llvm.coro.end.async(ptr <handle>, i1 <unwind>, ...)
概要:

llvm.coro.end.async’ は、コルーチンの再開部分の実行が終了し、制御が呼び出し元に戻るべきポイントを示します。可変長の末尾引数の一部として、この命令を使用すると、戻る前の最後のアクションとして末尾呼び出しされる関数と関数の引数を指定できます。

引数:

最初の引数は、囲んでいるコルーチンのコルーチンハンドルを参照する必要があります。フロントエンドは、最初のパラメータとしてnullを提供できます。この場合、coro-earlyパスは、nullを適切なコルーチンハンドル値に置き換えます。

2番目の引数は、このcoro.endが例外のためにコルーチン本体を離れるアンワインドシーケンスの一部であるブロックにある場合はtrue、それ以外の場合はfalseである必要があります。

3番目の引数が存在する場合、呼び出される関数を指定する必要があります。

3番目の引数が存在する場合、残りの引数は関数呼び出しへの引数です。

call i1 (ptr, i1, ...) @llvm.coro.end.async(
                         ptr %hdl, i1 0,
                         ptr @must_tail_call_return,
                         ptr %ctxt, ptr %task, ptr %actor)
unreachable

‘llvm.coro.suspend’ 組み込み関数

declare i8 @llvm.coro.suspend(token <save>, i1 <final>)
概要:

llvm.coro.suspend’ は、切り替え再開コルーチンの実行が中断され、制御が呼び出し元に戻されるポイントを示します。この組み込み関数の結果を消費する条件分岐は、コルーチンが中断 (-1)、再開 (0)、または破棄 (1) されるときに進む基本ブロックにつながります。

引数:

最初の引数は、コルーチン状態が中断のために準備された時点を示す coro.save 組み込み関数のトークンを参照します。none トークンが渡された場合、この組み込み関数は、coro.suspend 組み込み関数の直前に coro.save があったかのように動作します。

2番目の引数は、この中断ポイントが 最終であるかどうかを示します。2番目の引数は定数のみを受け入れます。複数の中断ポイントが最終として指定されている場合、再開と破棄の分岐は同じ基本ブロックにつながる必要があります。

例(通常の中断ポイント):
%0 = call i8 @llvm.coro.suspend(token none, i1 false)
switch i8 %0, label %suspend [i8 0, label %resume
                              i8 1, label %cleanup]
例(最終中断ポイント):
while.end:
  %s.final = call i8 @llvm.coro.suspend(token none, i1 true)
  switch i8 %s.final, label %suspend [i8 0, label %trap
                                      i8 1, label %cleanup]
trap:
  call void @llvm.trap()
  unreachable
セマンティクス:

この組み込み関数でマークされた中断ポイントで中断されたコルーチンが coro.resume によって再開された場合、制御は0の場合の基本ブロックに転送されます。coro.destroy によって再開された場合は、1の場合によって示される基本ブロックに進みます。中断するには、コルーチンはデフォルトラベルに進みます。

中断組み込み関数が最終としてマークされている場合、true ブランチが到達不能であると見なし、その事実を利用できる最適化を実行できます。

‘llvm.coro.save’ 組み込み関数

declare token @llvm.coro.save(ptr <handle>)
概要:

llvm.coro.save’ は、コルーチンが中断されたと見なされるために(したがって再開の対象となる)、再開のために状態を更新する必要があるポイントを示します。‘llvm.coro.save’ の2つの呼び出しは、それらの ‘llvm.coro.suspend’ のユーザーもマージされない限り、マージすることはできません。したがって、‘llvm.coro.save’ は現在、no_merge 関数属性でタグ付けされています。

引数:

最初の引数は、囲みコルーチンのコルーチンハンドルを指します。

セマンティクス:

対応する中断ポイントからのコルーチンの再開を有効にするために必要なコルーチン状態の変更はすべて、coro.save 組み込み関数の時点で行う必要があります。

例:

コルーチンが、非同期操作の完了を表すコールバックによって駆動される非同期制御フローを表すために使用される場合、個別の保存ポイントと中断ポイントが必要です。

そのような場合、コルーチンは async_op 関数を呼び出す前に再開の準備ができている必要があります。これにより、コルーチンの制御が戻る前に、同じスレッドまたは別のスレッドからコルーチンの再開がトリガーされる可能性があります。

%save1 = call token @llvm.coro.save(ptr %hdl)
call void @async_op1(ptr %hdl)
%suspend1 = call i1 @llvm.coro.suspend(token %save1, i1 false)
switch i8 %suspend1, label %suspend [i8 0, label %resume1
                                     i8 1, label %cleanup]

‘llvm.coro.suspend.async’ 組み込み関数

declare {ptr, ptr, ptr} @llvm.coro.suspend.async(
                           ptr <resume function>,
                           ptr <context projection function>,
                           ... <function to call>
                           ... <arguments to function>)
概要:

llvm.coro.suspend.async’ 組み込み関数は、非同期コルーチンの実行が中断され、制御が呼び出し先に渡されるポイントを示します。

引数:

最初の引数は、llvm.coro.async.resume 組み込み関数の結果である必要があります。低レベル化では、この組み込み関数がこの中断ポイントの再開関数に置き換えられます。

2番目の引数は、コンテキストプロジェクション関数です。継続関数の最初の引数から継続関数で 非同期コンテキスト を復元する方法を記述する必要があります。その型は ptr (ptr) です。

3番目の引数は、中断ポイントで呼び出し先への転送をモデル化する関数です。3つの引数を受け取る必要があります。低レベル化では、この関数を musttail 呼び出しします。

4番目から6番目の引数は、3番目の引数の引数です。

セマンティクス:

組み込み関数の結果は、再開関数の引数にマップされます。実行はこの組み込み関数で中断され、再開関数が呼び出されると再開されます。

‘llvm.coro.prepare.async’ 組み込み関数

declare ptr @llvm.coro.prepare.async(ptr <coroutine function>)
概要:

llvm.coro.prepare.async’ 組み込み関数は、コルーチンの分割後まで非同期コルーチンのインライン化をブロックするために使用されます。

引数:

最初の引数は、型 void (ptr, ptr, ptr) の非同期コルーチンである必要があります。低レベル化では、この組み込み関数がそのコルーチン関数引数に置き換えられます。

‘llvm.coro.suspend.retcon’ 組み込み関数

declare i1 @llvm.coro.suspend.retcon(...)
概要:

llvm.coro.suspend.retcon’ 組み込み関数は、返された継続コルーチンの実行が中断され、制御が呼び出し元に戻されるポイントを示します。

llvm.coro.suspend.retcon` は個別の保存ポイントをサポートしていません。継続関数がローカルにアクセスできない場合、それらは役に立ちません。それは、まだ実装されていない passcon 低レベル化のためにより適切な機能でしょう。

引数:

引数の型は、コルーチンの yield された型シーケンスと正確に一致する必要があります。それらは、次の継続関数とともに、ランプ関数と継続関数からの戻り値に変換されます。

セマンティクス:

組み込み関数の結果は、コルーチンが異常に再開されるかどうか(ゼロ以外)を示します。

通常のコルーチンでは、コルーチンが異常に再開された後に llvm.coro.suspend.retcon の呼び出しを実行した場合、動作は未定義です。

yield-once コルーチンでは、コルーチンがいかなる方法で再開された後も llvm.coro.suspend.retcon の呼び出しを実行した場合、動作は未定義です。

‘llvm.coro.await.suspend.void’ 組み込み関数

declare void @llvm.coro.await.suspend.void(
              ptr <awaiter>,
              ptr <handle>,
              ptr <await_suspend_function>)
概要:

llvm.coro.await.suspend.void’ 組み込み関数は、コルーチンの変換を妨げなくなるまで、C++ await-suspend ブロックをカプセル化します。

co_awaitawait_suspend ブロックは、本質的にコルーチンの実行に対して非同期です。通常、分割されていないコルーチンにインライン化すると、コルーチンCFGがプログラムの真の制御フローを誤って表現するため、コンパイルミスが発生する可能性があります。await_suspend で発生する事柄は、コルーチンの再開前に発生することが保証されておらず、コルーチンの再開後に発生する事柄(終了やコルーチンフレームの潜在的な割り当て解除を含む)は、await_suspend の終了後にのみ発生することが保証されていません。

このバージョンの組み込み関数は、‘void awaiter.await_suspend(...)’ バリアントに対応します。

引数:

最初の引数は、awaiter オブジェクトへのポインタです。

2番目の引数は、現在のコルーチンフレームへのポインタです。

3番目の引数は、await-suspend ロジックをカプセル化するラッパー関数へのポインタです。そのシグネチャは次のようになっている必要があります。

declare void @await_suspend_function(ptr %awaiter, ptr %hdl)
セマンティクス:

組み込み関数は、対応する coro.save 呼び出しと coro.suspend 呼び出しの間で使用する必要があります。CoroSplit パスの間に、直接 await_suspend_function 呼び出しに低レベル化されます。

例:
; before lowering
await.suspend:
  %save = call token @llvm.coro.save(ptr %hdl)
  call void @llvm.coro.await.suspend.void(
              ptr %awaiter,
              ptr %hdl,
              ptr @await_suspend_function)
  %suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
  ...

; after lowering
await.suspend:
  %save = call token @llvm.coro.save(ptr %hdl)
  ; the call to await_suspend_function can be inlined
  call void @await_suspend_function(
              ptr %awaiter,
              ptr %hdl)
  %suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
  ...

; wrapper function example
define void @await_suspend_function(ptr %awaiter, ptr %hdl)
  entry:
    %hdl.arg = ... ; construct std::coroutine_handle from %hdl
    call void @"Awaiter::await_suspend"(ptr %awaiter, ptr %hdl.arg)
    ret void

‘llvm.coro.await.suspend.bool’ 組み込み関数

declare i1 @llvm.coro.await.suspend.bool(
              ptr <awaiter>,
              ptr <handle>,
              ptr <await_suspend_function>)
概要:

llvm.coro.await.suspend.bool’ 組み込み関数は、コルーチンの変換を妨げなくなるまで、C++ await-suspend ブロックをカプセル化します。

co_awaitawait_suspend ブロックは、本質的にコルーチンの実行に対して非同期です。通常、分割されていないコルーチンにインライン化すると、コルーチンCFGがプログラムの真の制御フローを誤って表現するため、コンパイルミスが発生する可能性があります。await_suspend で発生する事柄は、コルーチンの再開前に発生することが保証されておらず、コルーチンの再開後に発生する事柄(終了やコルーチンフレームの潜在的な割り当て解除を含む)は、await_suspend の終了後にのみ発生することが保証されていません。

このバージョンの組み込み関数は、‘bool awaiter.await_suspend(...)’ バリアントに対応します。

引数:

最初の引数は、awaiter オブジェクトへのポインタです。

2番目の引数は、現在のコルーチンフレームへのポインタです。

3番目の引数は、await-suspend ロジックをカプセル化するラッパー関数へのポインタです。そのシグネチャは次のようになっている必要があります。

declare i1 @await_suspend_function(ptr %awaiter, ptr %hdl)
セマンティクス:

組み込み関数は、対応する coro.save 呼び出しと coro.suspend 呼び出しの間で使用する必要があります。CoroSplit パスの間に、直接 await_suspend_function 呼び出しに低レベル化されます。

await_suspend_function 呼び出しが true を返す場合、現在のコルーチンは直ちに再開されます。

例:
; before lowering
await.suspend:
  %save = call token @llvm.coro.save(ptr %hdl)
  %resume = call i1 @llvm.coro.await.suspend.bool(
              ptr %awaiter,
              ptr %hdl,
              ptr @await_suspend_function)
  br i1 %resume, %await.suspend.bool, %await.ready
await.suspend.bool:
  %suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
  ...
await.ready:
  call void @"Awaiter::await_resume"(ptr %awaiter)
  ...

; after lowering
await.suspend:
  %save = call token @llvm.coro.save(ptr %hdl)
  ; the call to await_suspend_function can inlined
  %resume = call i1 @await_suspend_function(
              ptr %awaiter,
              ptr %hdl)
  br i1 %resume, %await.suspend.bool, %await.ready
  ...

; wrapper function example
define i1 @await_suspend_function(ptr %awaiter, ptr %hdl)
  entry:
    %hdl.arg = ... ; construct std::coroutine_handle from %hdl
    %resume = call i1 @"Awaiter::await_suspend"(ptr %awaiter, ptr %hdl.arg)
    ret i1 %resume

‘llvm.coro.await.suspend.handle’ 組み込み関数

declare void @llvm.coro.await.suspend.handle(
              ptr <awaiter>,
              ptr <handle>,
              ptr <await_suspend_function>)
概要:

llvm.coro.await.suspend.handle’ 組み込み関数は、コルーチンの変換を妨げなくなるまで、C++ await-suspend ブロックをカプセル化します。

co_awaitawait_suspend ブロックは、本質的にコルーチンの実行に対して非同期です。通常、分割されていないコルーチンにインライン化すると、コルーチンCFGがプログラムの真の制御フローを誤って表現するため、コンパイルミスが発生する可能性があります。await_suspend で発生する事柄は、コルーチンの再開前に発生することが保証されておらず、コルーチンの再開後に発生する事柄(終了やコルーチンフレームの潜在的な割り当て解除を含む)は、await_suspend の終了後にのみ発生することが保証されていません。

このバージョンの組み込み関数は、‘std::corouine_handle<> awaiter.await_suspend(...)’ バリアントに対応します。

引数:

最初の引数は、awaiter オブジェクトへのポインタです。

2番目の引数は、現在のコルーチンフレームへのポインタです。

3番目の引数は、await-suspend ロジックをカプセル化するラッパー関数へのポインタです。そのシグネチャは次のようになっている必要があります。

declare ptr @await_suspend_function(ptr %awaiter, ptr %hdl)
セマンティクス:

組み込み関数は、対応する coro.save 呼び出しと coro.suspend 呼び出しの間で使用する必要があります。CoroSplit パスの間に、直接 await_suspend_function 呼び出しに低レベル化されます。

await_suspend_function は、有効なコルーチンフレームへのポインタを返す必要があります。組み込み関数は、返されたコルーチンフレームを再開する末尾呼び出しに低レベル化されます。それをサポートするターゲットでは、musttail とマークされます。組み込み関数の後の命令は到達不能になります。

例:
; before lowering
await.suspend:
  %save = call token @llvm.coro.save(ptr %hdl)
  call void @llvm.coro.await.suspend.handle(
      ptr %awaiter,
      ptr %hdl,
      ptr @await_suspend_function)
  %suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
  ...

; after lowering
await.suspend:
  %save = call token @llvm.coro.save(ptr %hdl)
  ; the call to await_suspend_function can be inlined
  %next = call ptr @await_suspend_function(
              ptr %awaiter,
              ptr %hdl)
  musttail call void @llvm.coro.resume(%next)
  ret void
  ...

; wrapper function example
define ptr @await_suspend_function(ptr %awaiter, ptr %hdl)
  entry:
    %hdl.arg = ... ; construct std::coroutine_handle from %hdl
    %hdl.raw = call ptr @"Awaiter::await_suspend"(ptr %awaiter, ptr %hdl.arg)
    %hdl.result = ... ; get address of returned coroutine handle
    ret ptr %hdl.result

コルーチン変換パス

CoroEarly

CoroEarly パスは、コルーチンフレームの構造の詳細を隠すコルーチン組み込み関数を低レベル化しますが、それ以外の場合は後続のコルーチンパスを支援するために保存する必要はありません。このパスは、coro.framecoro.done、および coro.promise 組み込み関数を低レベル化します。

CoroSplit

パス CoroSplit は、コルーチンフレームを構築し、再開部分と破棄部分を別々の関数にアウトライン化します。このパスは、coro.await.suspend.voidcoro.await.suspend.boolcoro.await.suspend.handle 組み込み関数も低レベル化します。

CoroElide

パス CoroElide は、インライン化されたコルーチンがヒープ割り当て省略最適化の対象となるかどうかを調べます。対象となる場合、coro.begin 組み込み関数を呼び出し元に配置されたコルーチンフレームのアドレスに置き換え、coro.alloc および coro.free 組み込み関数をそれぞれ false および null に置き換えて、解放コードを削除します。このパスは、可能であれば、特定のコルーチンの再開関数および破棄関数への直接呼び出しで coro.resume および coro.destroy 組み込み関数も置き換えます。

CoroCleanup

このパスは、早期のパスで置き換えられなかったすべてのコルーチン関連の組み込み関数を低レベル化するために、遅れて実行されます。

属性

coro_only_destroy_when_complete

コルーチンが coro_only_destroy_when_complete でマークされている場合、コルーチンが破棄されるときに最終的な中断点に到達する必要があることを示します。

この属性は、現在、切り替えられた再開コルーチンでのみ機能します。

メタデータ

coro.outside.frame’ メタデータ

coro.outside.frame メタデータは、alloca 命令にアタッチして、コルーチンフレームに昇格させないことを示すことができます。これは、内部制御メカニズムをエミットする際に、フロントエンドが alloca をフィルタリングするのに役立ちます。さらに、このメタデータはフラグとしてのみ使用されるため、関連付けられたノードは空である必要があります。

%__coro_gro = alloca %struct.GroType, align 1, !coro.outside.frame !0

...
!0 = !{}

注意が必要な領域

  1. coro.suspend が -1 を返した場合、コルーチンは中断され、コルーチンがすでに破棄されている(したがってフレームが解放されている)可能性があります。中断パスでは、フレーム上のものにアクセスすることはできません。ただし、コンパイラーがそのパスに沿って命令を移動する(例えば、LICM)のを防ぐものは何もありません。これは、use-after-freeにつながる可能性があります。現時点では、coro.suspend を持つループの LICM を無効にしましたが、一般的な問題は依然として存在し、一般的な解決策が必要です。

  2. コルーチンフレームに入るデータのライフタイム組み込み関数を利用してください。alloca に残るデータのライフタイム組み込み関数はそのままにします。

  3. CoroElide 最適化パスは、コルーチンのランプ関数がインライン化されることに依存しています。ランプ関数をさらに分割して、呼び出し元にインライン化される可能性を高めることが有益です。

  4. ABI の境界を越えてコルーチンヒープ省略最適化を適用できるようにする規則を設計します。

  5. inalloca パラメータ(Windows の x86 で使用)を持つコルーチンを処理できません。

  6. アラインメントは、coro.begin および coro.free 組み込み関数によって無視されます。

  7. コルーチンの最適化が LTO で機能するように、必要な変更を加えます。

  8. より多くのテスト、より多くのテスト、より多くのテスト