The Problem:
Instead of using a stack frame, Coroutines (we will use Swift async frames as the running example) store local variables in a heap data structure called the coroutine frame. To describe variables in a coroutine frame we would want the semantics of #dbg_declares instead of #dbg_values, because of the guarantee that there should only be one #dbg_declare for a variable in a function. However, because it is meant to refer to stack frames, the location of a #dbg_declare must be a pointer. That is typically true after the CoroSplitter pass runs, but not before. This causes a dilemma for the Swift frontend when it wants to describe local variables that are expected to get allocated in the coroutine frame in pre-corosplit LLVM IR.
Example:
// test.swift
public func foo(yyyy: Double) async
{
print(yyyy)
}
if I run the command:
swiftc -g -emit-irgen test.swift -O -o -
Which will produce the LLVM IR that the swift compiler generates just before handing it over to LLVM, we can see:
; Function Attrs: noinline
define swifttailcc void @"$s4test3foo4yyyyySd_tYaF"(ptr swiftasync %0, double %1) #1 !dbg !43
{
entry:
#dbg_declare(double %1, !50, !DIExpression(), !52)
...
}
Here, the #dbg_declare contains a location with the type double, however, if we look at the output of the CoroSplitterPass by using the -print-after-all
output, we can see
; *** IR Dump After CoroSplitPass on ($s4test3foo4yyyyySd_tYaF) ***
; Function Attrs: noinline
define swifttailcc void @"$s4test3foo4yyyyySd_tYaF"(ptr swiftasync %0, double %1) #1 !dbg !35
{
#dbg_declare(ptr %0, !42, !DIExpression(DW_OP_plus_uconst, 16), !44)
%3 = getelementptr inbounds i8, ptr %0, i32 16
...
7: ; preds = %6
%8 = alloca double, align 8, !dbg !45
#dbg_declare(ptr %8, !42, !DIExpression(DW_OP_deref), !45)
}
We can see that the CoroSplitter pass fixes up the #dbg_declare and replaces the double with an alloca
and a ptr
However, with commit 20507a9e95a08069863e9910a688a38370d58952 there is a new check in the verifier that ensures that #dbg_declare always have a location that is a pointer. This causes an immediate crash in the compiler when swift hands off the generated IR to LLVM.
The Solution:
To fix this, we propose a new intrinsic called llvm.dbg.coroframe_entry or #dbg_coroframe_entry. This intrinsic will behave exactly like a #dbg_declare, however, it will not have the restriction of a ptr only location as a #dbg_declare.
The CoroSplit pass will transform any #dbg_coroframe_entry intrinsics just like it did with #dbg_declares, however, crucially, after fixing up the location type, it will also re-emit them as #dbg_declares, so that we donāt have to teach subsequent passes, such as InstCombine, about what a #dbg_coroframe_entry is
Example:
// test.swift
public func foo(yyyy: Double) async
{
print(yyyy)
}
if I run the command:
swiftc -g -emit-irgen test.swift -O -o -
Which will produce the LLVM IR that the swift compiler generates just before handing it over to LLVM, we can see:
; Function Attrs: noinline
define swifttailcc void @"$s4test3foo4yyyyySd_tYaF"(ptr swiftasync %0, double %1) #1 !dbg !43
{
entry:
#dbg_coroframe_entry(double %1, !50, !DIExpression(), !52)
...
}
Here, the #dbg_coroframe_entry contains a location with the type double, but the Verifier does not care, and therefore we have no issues handing off this IR to LLVM
When we look at the -print-after-all
output, we can see:
; *** IR Dump After CoroSplitPass on ($s4test3foo4yyyyySd_tYaF) ***
; Function Attrs: noinline
define swifttailcc void @"$s4test3foo4yyyyySd_tYaF"(ptr swiftasync %0, double %1) #1 !dbg !35
{
#dbg_declare(ptr %0, !42, !DIExpression(DW_OP_plus_uconst, 16), !44)
%3 = getelementptr inbounds i8, ptr %0, i32 16
...
7: ; preds = %6
%8 = alloca double, align 8, !dbg !45
#dbg_declare(ptr %8, !42, !DIExpression(DW_OP_deref), !45)
}
The CoroSplitter pass has transformed the #dbg_coroframe_entry into #dbg_declares and also fixed up the double location to a ptr.
TL;DR:
introduce a new dbg_coroframe_entry intrinsic to communicate local variable storage between a frontend and the CoroSplitter pass. The CoroSplitter spills local variables onto a heap data structure and lowers each dbg_coroframe_entry into a dbg_declare.