This document specifies how suspend points are lowered to LLVM IR, matching Kotlin/Native's coroutine implementation pattern.
The patterns here are derived from:
kotlin-native/.../llvm/IrToBitcode.kt-evaluateSuspendableExpression(),evaluateSuspensionPoint()kotlin-native/.../lower/CoroutinesVarSpillingLowering.kt- variable spilling
Suspend functions use computed goto (indirectbr + blockaddress) for resumption:
- Entry dispatch: Check if
_labelis null (first call) or contains a resume address - Suspend point: Store
blockaddress(@func, %resume_label)to_label, returnCOROUTINE_SUSPENDED - Resume:
indirectbrjumps directly to the stored label
The transformation is implemented via CMake modules in cmake/Modules/:
| File | Purpose |
|---|---|
kxs_transform_ir.cmake |
Standalone IR transformation script (invoked via cmake -P) |
KotlinxCoroutineTransform.cmake |
CMake functions: kxs_transform_ir(), kxs_enable_coroutine_transform() |
KotlinxCoroutines.cmake |
Interface library kotlinx::coroutines and kxs_enable_suspend() |
include(KotlinxCoroutines)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE kotlinx::coroutines)
kxs_enable_suspend(my_app) # Enable IR transformationOr use the convenience macro:
kxs_add_executable(my_app SOURCES main.cpp)┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌─────────┐
│ foo.cpp │ --> │ foo.ll │ --> │ foo.kxs.ll │ --> │ foo.o │
│ (C++ src) │ │ (LLVM IR text) │ │ (transformed) │ │ (object)│
└─────────────┘ └──────────────────┘ └─────────────────┘ └─────────┘
clang -emit-llvm -S cmake -P clang -c
kxs_transform_ir.cmake
- Finds calls to
__kxs_suspend_point(i32 N)in the IR - Generates resume labels:
kxs.resume.0,kxs.resume.1, ... - Inserts entry dispatch block with
indirectbr - Replaces each suspend call with
blockaddressstore + resume label - Outputs transformed IR with header comment
Uses Clang labels-as-values extension (&&label). Generates indirectbr + blockaddress in LLVM IR,
matching Kotlin/Native exactly.
class MyCoroutine : public ContinuationImpl {
void* _label = nullptr; // blockaddress storage
void* invoke_suspend(Result<void*> result) override {
coroutine_begin(this)
// ... code ...
coroutine_yield(this, some_suspend_call(completion_));
// ... more code ...
coroutine_end(this)
}
};The macros use __LINE__ to generate unique labels at each suspend point:
#define _KXS_CONCAT(a, b) a##b
#define _KXS_LABEL(prefix, line) _KXS_CONCAT(prefix, line)
#define coroutine_yield(c, expr) \
do { \
(c)->_label = &&_KXS_LABEL(_kxs_resume_, __LINE__); \
::__kxs_suspend_point(__LINE__); \
{ \
auto _kxs_tmp = (expr); \
if (::kotlinx::coroutines::intrinsics::is_coroutine_suspended(_kxs_tmp)) \
return _kxs_tmp; \
} \
goto _KXS_LABEL(_kxs_cont_, __LINE__); \
_KXS_LABEL(_kxs_resume_, __LINE__): \
(void)(result).get_or_throw(); \
_KXS_LABEL(_kxs_cont_, __LINE__):; \
} while (0)At line 42, this expands to:
(c)->_label = &&_kxs_resume_42; // Store blockaddress
__kxs_suspend_point(42); // IR marker (found by CMake transform)
auto _kxs_tmp = (expr);
if (is_coroutine_suspended(_kxs_tmp))
return _kxs_tmp;
goto _kxs_cont_42;
_kxs_resume_42: // Resume label
(void)(result).get_or_throw();
_kxs_cont_42:; // Continuation// Declared in Suspend.hpp
extern "C" void __kxs_suspend_point(int id);This function:
- Is a no-op at runtime (can be empty or just return)
- Serves as an IR-visible marker that survives compilation
- Is found by
kxs_transform_ir.cmakevia regex:call void @__kxs_suspend_point\(i32[^)]*\) - The
idargument (from__LINE__) identifies each suspend point
define ptr @my_coroutine(ptr %coro, ptr %result) {
entry:
; ... setup ...
call void @__kxs_suspend_point(i32 42)
; ... more code ...
call void @__kxs_suspend_point(i32 57)
; ...
}; KXS-TRANSFORMED: 2 suspend points
; Resume labels: kxs.resume.0;kxs.resume.1
; Dispatch: indirectbr + blockaddress (Kotlin/Native pattern)
define ptr @my_coroutine(ptr %coro, ptr %result) {
kxs.entry:
; KXS: Allocate label slot for computed goto
%kxs.label.slot = alloca ptr, align 8
store ptr null, ptr %kxs.label.slot, align 8
; KXS: Load resume label (null on first call)
%kxs.saved.label = load ptr, ptr %kxs.label.slot, align 8
%kxs.is.first = icmp eq ptr %kxs.saved.label, null
br i1 %kxs.is.first, label %kxs.start, label %kxs.dispatch
kxs.dispatch:
; KXS: Resume via computed goto (indirectbr)
indirectbr ptr %kxs.saved.label, [label %kxs.resume.0, label %kxs.resume.1]
kxs.start:
; ... original entry code ...
; KXS: Store resume address (blockaddress pattern)
store ptr blockaddress(@my_coroutine, %kxs.resume.0), ptr %kxs.label.slot, align 8
; KXS: suspend point 42 - actual suspend call would precede this
br label %kxs.resume.0
kxs.resume.0: ; KXS resume point 42
; ... code continues ...
; KXS: Store resume address (blockaddress pattern)
store ptr blockaddress(@my_coroutine, %kxs.resume.1), ptr %kxs.label.slot, align 8
; KXS: suspend point 57
br label %kxs.resume.1
kxs.resume.1: ; KXS resume point 57
; ...
}The CMake script (cmake -P kxs_transform_ir.cmake) performs these steps:
string(REGEX MATCHALL "call void @__kxs_suspend_point\\(i32[^)]*\\)" SUSPEND_CALLS "${IR}")string(REGEX MATCH "define [^@]+@([A-Za-z_][A-Za-z0-9_]*)" FUNC_MATCH "${IR}")set(IDX 0)
while(IDX LESS N_SUSPEND)
list(APPEND LABELS "kxs.resume.${IDX}")
math(EXPR IDX "${IDX} + 1")
endwhile()# Generates: label %kxs.resume.0, label %kxs.resume.1, ...
foreach(L ${LABELS})
string(APPEND LABEL_LIST "label %${L}")
endforeach()Each call void @__kxs_suspend_point(i32 N) is replaced with:
; KXS: Store resume address (blockaddress pattern)
store ptr blockaddress(@${FUNC_NAME}, %${RESUME_LABEL}), ptr %kxs.label.slot, align 8
; KXS: suspend point ${SID}
br label %${RESUME_LABEL}
${RESUME_LABEL}: ; KXS resume point ${SID}After the function opening {, insert:
kxs.entry:
%kxs.label.slot = alloca ptr, align 8
store ptr null, ptr %kxs.label.slot, align 8
%kxs.saved.label = load ptr, ptr %kxs.label.slot, align 8
%kxs.is.first = icmp eq ptr %kxs.saved.label, null
br i1 %kxs.is.first, label %kxs.start, label %kxs.dispatch
kxs.dispatch:
indirectbr ptr %kxs.saved.label, [${LABEL_LIST}]
kxs.start:From CoroutinesVarSpillingLowering.kt:
At each suspend point, live variables must be saved to the coroutine struct before suspension and restored after resumption:
// Before suspend:
saveCoroutineState -> {
for (variable in liveVariables) {
coroutine.field[variable] = variable
}
}
// After resume:
restoreCoroutineState -> {
for (variable in liveVariables) {
variable = coroutine.field[variable]
}
}This requires liveness analysis to determine which variables cross suspend points.
Current approach: Manual spilling - developers declare spill fields in their ContinuationImpl class.
Future: kxs-inject tool will perform liveness analysis and generate spill code automatically.
The coroutine struct must have _label as a field (typically first for efficiency):
struct MyCoroutine : public ContinuationImpl {
void* _label = nullptr; // blockaddress storage (Kotlin/Native NativePtr)
// ... spilled variables ...
int saved_count;
std::string saved_name;
};In LLVM IR terms:
%Coroutine = type { ptr, ... } ; first field is label pointerTests using the DSL macros need:
target_compile_options(my_test PRIVATE
-Wno-gnu-label-as-value # Allow &&label (labels as values)
)- Kotlin/Native IrToBitcode.kt lines 2377-2424
- Kotlin/Native CoroutinesVarSpillingLowering.kt
- Clang Labels as Values: https://clang.llvm.org/docs/LanguageExtensions.html#labels-as-values
- LLVM indirectbr: https://llvm.org/docs/LangRef.html#indirectbr-instruction
- CMake Modules:
cmake/Modules/kxs_transform_ir.cmake,KotlinxCoroutineTransform.cmake,KotlinxCoroutines.cmake