ソースを参照

Rudimentary virtual function call interop support (#6050)

This is Itanium-specific for now (explicitly downcasting to the itanium
vtable handling code in Clang) - though it doesn't look like it'd be a
big stretch to either have conditional/two codepaths down Itanium and
MSVC in Carbon, or maybe add a virtual function in clang to avoid
needing to conditional+downcast in Carbon.

Here's a working example:
`dynamic_type.h`:
```
#ifndef TEST_H
#define TEST_H

struct A {
  virtual auto virt0() -> int;
  virtual auto virt1() -> int;
};

auto GetVal() -> A* _Nonnull;

#endif
```
`test.carbon`:
```
library "test";

import Cpp library "dynamic_type.h";
import Core library "io";

fn Run() {
  var a: Cpp.A* = Cpp.GetVal();
  Core.Print(a->virt0());
  Core.Print(a->virt1());
}
```
`dynamic_type.cpp`:
```
#include "dynamic_type.h"

auto A::virt0() -> int {
  return 0;
}

auto A::virt1() -> int {
  return 1;
}

struct B: A {
  auto virt0() -> int override {
    return 7;
  }
  auto virt1() -> int override {
    return 42;
  }
};

auto GetVal() -> A* _Nonnull {
  static B b;
  return &b;
}
```
```
$ ./bazel-bin/toolchain/carbon compile test.carbon
$ clang++-tot -g dynamic_type.cpp test.o --output=a.out
$ ./a.out
7
42
```
(linking with `carbon link` failed because we aren't linking to the C++
runtime yet, it seems, so: `ld.lld: error: undefined symbol: vtable for
__cxxabiv1::__class_type_info`)

---------

Co-authored-by: Dana Jansens <danakj@orodu.net>
David Blaikie 7 ヶ月 前
コミット
bff0e5978b

+ 18 - 9
toolchain/check/cpp/import.cpp

@@ -13,6 +13,7 @@
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/RecordLayout.h"
 #include "clang/AST/UnresolvedSet.h"
+#include "clang/AST/VTableBuilder.h"
 #include "clang/Basic/FileManager.h"
 #include "clang/Frontend/ASTUnit.h"
 #include "clang/Frontend/CompilerInstance.h"
@@ -1623,6 +1624,21 @@ static auto ImportFunction(Context& context, SemIR::LocId loc_id,
       AddPlaceholderInstInNoBlock(context, Parse::NodeId::None, function_decl);
   context.imports().push_back(decl_id);
 
+  auto virtual_modifier = SemIR::Function::VirtualModifier::None;
+  int32_t virtual_index = -1;
+  if (auto* method_decl = dyn_cast<clang::CXXMethodDecl>(clang_decl)) {
+    if (method_decl->size_overridden_methods()) {
+      virtual_modifier = SemIR::Function::VirtualModifier::Override;
+    } else if (method_decl->isVirtual()) {
+      virtual_modifier = SemIR::Function::VirtualModifier::Virtual;
+    }
+    if (virtual_modifier != SemIR::Function::VirtualModifier::None) {
+      // TODO: Add support for Microsoft/non-Itanium vtables.
+      virtual_index = dyn_cast<clang::ItaniumVTableContext>(
+                          context.ast_context().getVTableContext())
+                          ->getMethodVTableIndex(method_decl);
+    }
+  }
   auto function_info = SemIR::Function{
       {.name_id = GetFunctionName(context, clang_decl),
        .parent_scope_id = GetParentNameScopeId(context, clang_decl),
@@ -1640,7 +1656,8 @@ static auto ImportFunction(Context& context, SemIR::LocId loc_id,
        .definition_id = SemIR::InstId::None},
       {.call_params_id = function_params_insts->call_params_id,
        .return_slot_pattern_id = function_params_insts->return_slot_pattern_id,
-       .virtual_modifier = SemIR::FunctionFields::VirtualModifier::None,
+       .virtual_modifier = virtual_modifier,
+       .virtual_index = virtual_index,
        .self_param_id = FindSelfPattern(
            context, function_params_insts->implicit_param_patterns_id),
        .clang_decl_id = context.sem_ir().clang_decls().Add(
@@ -1675,14 +1692,6 @@ auto ImportCppFunctionDecl(Context& context, SemIR::LocId loc_id,
     return SemIR::ErrorInst::InstId;
   }
 
-  if (auto* method_decl = dyn_cast<clang::CXXMethodDecl>(clang_decl)) {
-    if (method_decl->isVirtual()) {
-      context.TODO(loc_id, "Unsupported: Virtual function");
-      MarkFailedDecl(context, clang_decl);
-      return SemIR::ErrorInst::InstId;
-    }
-  }
-
   CARBON_CHECK(clang_decl->getFunctionType()->isFunctionProtoType(),
                "Not Prototype function (non-C++ code)");
 

+ 19 - 16
toolchain/check/testdata/interop/cpp/class/struct.carbon

@@ -169,7 +169,7 @@ import Cpp library "dynamic.h";
 fn MyF(bar: Cpp.Bar*);
 //@dump-sem-ir-end
 
-// --- fail_todo_call_dynamic.carbon
+// --- call_dynamic.carbon
 
 library "[[@TEST_NAME]]";
 
@@ -177,13 +177,6 @@ import Cpp library "dynamic.h";
 
 //@dump-sem-ir-begin
 fn MyF(bar: Cpp.Bar*) {
-  // CHECK:STDERR: fail_todo_call_dynamic.carbon:[[@LINE+7]]:3: error: semantics TODO: `Unsupported: Virtual function` [SemanticsTodo]
-  // CHECK:STDERR:   bar->f();
-  // CHECK:STDERR:   ^~~~~~~~
-  // CHECK:STDERR: fail_todo_call_dynamic.carbon:[[@LINE+4]]:3: note: in call to Cpp function here [InCallToCppFunction]
-  // CHECK:STDERR:   bar->f();
-  // CHECK:STDERR:   ^~~~~~~~
-  // CHECK:STDERR:
   bar->f();
 }
 //@dump-sem-ir-end
@@ -473,16 +466,19 @@ fn MyF(bar: Cpp.Bar*);
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @MyF(%bar.param: %ptr);
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_call_dynamic.carbon
+// CHECK:STDOUT: --- call_dynamic.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
 // CHECK:STDOUT:   %Bar: type = class_type @Bar [concrete]
 // CHECK:STDOUT:   %ptr.f68: type = ptr_type %Bar [concrete]
-// CHECK:STDOUT:   %pattern_type: type = pattern_type %ptr.f68 [concrete]
+// CHECK:STDOUT:   %pattern_type.146: type = pattern_type %ptr.f68 [concrete]
 // CHECK:STDOUT:   %MyF.type: type = fn_type @MyF [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
 // CHECK:STDOUT:   %MyF: %MyF.type = struct_value () [concrete]
-// CHECK:STDOUT:   %.2e2: type = cpp_overload_set_type @<null name> [concrete]
+// CHECK:STDOUT:   %.2e2: type = cpp_overload_set_type @Bar.f [concrete]
 // CHECK:STDOUT:   %empty_struct: %.2e2 = struct_value () [concrete]
+// CHECK:STDOUT:   %Bar.f.type: type = fn_type @Bar.f [concrete]
+// CHECK:STDOUT:   %Bar.f: %Bar.f.type = struct_value () [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -491,13 +487,18 @@ fn MyF(bar: Cpp.Bar*);
 // CHECK:STDOUT:     import Cpp//...
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Bar.decl: type = class_decl @Bar [concrete = constants.%Bar] {} {}
-// CHECK:STDOUT:   %.6e0: %.2e2 = cpp_overload_set_value @<null name> [concrete = constants.%empty_struct]
+// CHECK:STDOUT:   %.6e0: %.2e2 = cpp_overload_set_value @Bar.f [concrete = constants.%empty_struct]
+// CHECK:STDOUT:   %Bar.f.decl: %Bar.f.type = fn_decl @Bar.f [concrete = constants.%Bar.f] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
 // CHECK:STDOUT:   %MyF.decl: %MyF.type = fn_decl @MyF [concrete = constants.%MyF] {
-// CHECK:STDOUT:     %bar.patt: %pattern_type = binding_pattern bar [concrete]
-// CHECK:STDOUT:     %bar.param_patt: %pattern_type = value_param_pattern %bar.patt, call_param0 [concrete]
+// CHECK:STDOUT:     %bar.patt: %pattern_type.146 = binding_pattern bar [concrete]
+// CHECK:STDOUT:     %bar.param_patt: %pattern_type.146 = value_param_pattern %bar.patt, call_param0 [concrete]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %bar.param: %ptr.f68 = value_param call_param0
 // CHECK:STDOUT:     %.loc7: type = splice_block %ptr [concrete = constants.%ptr.f68] {
@@ -512,9 +513,11 @@ fn MyF(bar: Cpp.Bar*);
 // CHECK:STDOUT: fn @MyF(%bar.param: %ptr.f68) {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %bar.ref: %ptr.f68 = name_ref bar, %bar
-// CHECK:STDOUT:   %.loc15: ref %Bar = deref %bar.ref
+// CHECK:STDOUT:   %.loc8: ref %Bar = deref %bar.ref
 // CHECK:STDOUT:   %f.ref: %.2e2 = name_ref f, imports.%.6e0 [concrete = constants.%empty_struct]
-// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc15, %f.ref
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc8, %f.ref
+// CHECK:STDOUT:   %addr: %ptr.f68 = addr_of %.loc8
+// CHECK:STDOUT:   %Bar.f.call: init %empty_tuple.type = call imports.%Bar.f.decl(%addr)
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 58 - 27
toolchain/lower/handle_call.cpp

@@ -476,6 +476,60 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id,
   CARBON_FATAL("Unsupported builtin call.");
 }
 
+static auto HandleVirtualCall(FunctionContext& context,
+                              llvm::ArrayRef<llvm::Value*> args,
+                              const SemIR::File* callee_file,
+                              const SemIR::Function& function,
+                              const SemIR::CalleeFunction& callee_function)
+    -> llvm::CallInst* {
+  CARBON_CHECK(!args.empty(),
+               "Virtual functions must have at least one parameter");
+  auto* ptr_type =
+      llvm::PointerType::get(context.llvm_context(), /*AddressSpace=*/0);
+  // The vtable pointer is always at the start of the object in the Carbon
+  // ABI, so a pointer to the object is a pointer to the vtable pointer - load
+  // that to get a pointer to the vtable.
+  // TODO: Handle the case in C++ interop where the vtable pointer isn't at
+  // the start of the object.
+  // TODO: Use `context.LoadObject`.
+  auto* vtable = context.builder().CreateLoad(ptr_type, args.front(), "vtable");
+  auto* i32_type = llvm::IntegerType::getInt32Ty(context.llvm_context());
+  auto* pointer_type =
+      llvm::PointerType::get(context.llvm_context(), /* address space */ 0);
+  auto function_type_info =
+      context.GetFileContext(callee_file)
+          .BuildFunctionTypeInfo(function,
+                                 callee_function.resolved_specific_id);
+  llvm::Value* virtual_fn;
+  if (function.clang_decl_id.has_value()) {
+    // Use absolute vtables for clang interop - the itanium vtable contains
+    // function pointers.
+    auto* virtual_function_pointer_address = context.builder().CreateGEP(
+        pointer_type, vtable,
+        {llvm::ConstantInt::get(
+            i32_type, static_cast<uint64_t>(function.virtual_index))});
+    virtual_fn = context.builder().CreateLoad(
+        pointer_type, virtual_function_pointer_address, "memptr.virtualfn");
+  } else {
+    // For Carbon, use Relative VTables as pioneered by Fuchsia:
+    // https://llvm.org/devmtg/2021-11/slides/2021-RelativeVTablesinC.pdf
+    // In this case, the vtable contains an offset from the vtable itself to the
+    // function in question. This avoids the use of link-time relocations in the
+    // vtable (making object files smaller, improving link time) - at the cost
+    // of extra instructions to resolve the offset at the call-site.
+    // This uses the `llvm.load.relative` intrinsic (
+    // https://llvm.org/docs/LangRef.html#llvm-load-relative-intrinsic ) that
+    // essentially does the arithmetic in one-shot: ptr + *(ptr + offset)
+    virtual_fn = context.builder().CreateCall(
+        llvm::Intrinsic::getOrInsertDeclaration(
+            &context.llvm_module(), llvm::Intrinsic::load_relative, {i32_type}),
+        {vtable,
+         llvm::ConstantInt::get(
+             i32_type, static_cast<uint64_t>(function.virtual_index) * 4)});
+  }
+  return context.builder().CreateCall(function_type_info.type, virtual_fn,
+                                      args);
+}
 auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
                 SemIR::Call inst) -> void {
   llvm::ArrayRef<SemIR::InstId> arg_ids =
@@ -542,33 +596,7 @@ auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
   }
 
   llvm::CallInst* call;
-  if (function.virtual_index != -1) {
-    CARBON_CHECK(!args.empty(),
-                 "Virtual functions must have at least one parameter");
-    auto* ptr_type =
-        llvm::PointerType::get(context.llvm_context(), /*AddressSpace=*/0);
-    // The vtable pointer is always at the start of the object in the Carbon
-    // ABI, so a pointer to the object is a pointer to the vtable pointer - load
-    // that to get a pointer to the vtable.
-    // TODO: Use `context.LoadObject`.
-    auto* vtable =
-        context.builder().CreateLoad(ptr_type, args.front(), "vtable");
-    auto* i32_type = llvm::IntegerType::getInt32Ty(context.llvm_context());
-    auto function_type_info =
-        context.GetFileContext(callee_file)
-            .BuildFunctionTypeInfo(function,
-                                   callee_function.resolved_specific_id);
-    call = context.builder().CreateCall(
-        function_type_info.type,
-        context.builder().CreateCall(
-            llvm::Intrinsic::getOrInsertDeclaration(
-                &context.llvm_module(), llvm::Intrinsic::load_relative,
-                {i32_type}),
-            {vtable,
-             llvm::ConstantInt::get(
-                 i32_type, static_cast<uint64_t>(function.virtual_index) * 4)}),
-        args);
-  } else {
+  if (function.virtual_modifier == SemIR::Function::VirtualModifier::None) {
     auto* callee =
         context.GetFileContext(callee_file)
             .GetOrCreateFunction(callee_function.function_id,
@@ -590,6 +618,9 @@ auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
     CARBON_CHECK(callee->arg_size() == args.size(),
                  "Argument count mismatch: {0}", describe_call());
     call = context.builder().CreateCall(callee, args);
+  } else {
+    call = HandleVirtualCall(context, args, callee_file, function,
+                             callee_function);
   }
 
   context.SetLocal(inst_id, call);

+ 67 - 10
toolchain/lower/testdata/interop/cpp/method.carbon

@@ -13,9 +13,14 @@
 
 // --- methods.h
 
-struct A {
+struct Base {
+  virtual void virt0();
+};
+
+struct A: Base {
   int by_val() const { return n; }
   int by_ref() { return n; }
+  virtual void virt1();
   int n;
 };
 
@@ -39,6 +44,17 @@ fn UseVal(a: Cpp.A*) -> i32 {
   return a->by_ref();
 }
 
+// --- call_virtual.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp library "methods.h";
+
+fn UseVal(a: Cpp.A*) {
+  a->virt0();
+  a->virt1();
+}
+
 // --- thunk.h
 
 struct NeedThunk {
@@ -62,7 +78,8 @@ fn Call(n: Cpp.NeedThunk) {
 // CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
 // CHECK:STDOUT: target triple = "x86_64-unknown-linux-gnu"
 // CHECK:STDOUT:
-// CHECK:STDOUT: %struct.A = type { i32 }
+// CHECK:STDOUT: %struct.A = type <{ %struct.Base, i32, [4 x i8] }>
+// CHECK:STDOUT: %struct.Base = type { ptr }
 // CHECK:STDOUT:
 // CHECK:STDOUT: $_ZNK1A6by_valEv = comdat any
 // CHECK:STDOUT:
@@ -73,13 +90,13 @@ fn Call(n: Cpp.NeedThunk) {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: mustprogress noinline nounwind optnone
-// CHECK:STDOUT: define linkonce_odr dso_local i32 @_ZNK1A6by_valEv(ptr nonnull align 4 dereferenceable(4) %this) #0 comdat align 2 {
+// CHECK:STDOUT: define linkonce_odr dso_local i32 @_ZNK1A6by_valEv(ptr nonnull align 8 dereferenceable(12) %this) #0 comdat align 2 {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %this.addr = alloca ptr, align 8
 // CHECK:STDOUT:   store ptr %this, ptr %this.addr, align 8
 // CHECK:STDOUT:   %this1 = load ptr, ptr %this.addr, align 8
-// CHECK:STDOUT:   %n = getelementptr inbounds nuw %struct.A, ptr %this1, i32 0, i32 0
-// CHECK:STDOUT:   %0 = load i32, ptr %n, align 4
+// CHECK:STDOUT:   %n = getelementptr inbounds nuw %struct.A, ptr %this1, i32 0, i32 1
+// CHECK:STDOUT:   %0 = load i32, ptr %n, align 8
 // CHECK:STDOUT:   ret i32 %0
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -89,7 +106,7 @@ fn Call(n: Cpp.NeedThunk) {
 // CHECK:STDOUT:   %this.addr = alloca ptr, align 8
 // CHECK:STDOUT:   store ptr %this, ptr %this.addr, align 8
 // CHECK:STDOUT:   %0 = load ptr, ptr %this.addr, align 8
-// CHECK:STDOUT:   %call = call i32 @_ZNK1A6by_valEv(ptr nonnull align 4 dereferenceable(4) %0)
+// CHECK:STDOUT:   %call = call i32 @_ZNK1A6by_valEv(ptr nonnull align 8 dereferenceable(12) %0)
 // CHECK:STDOUT:   ret i32 %call
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -116,7 +133,8 @@ fn Call(n: Cpp.NeedThunk) {
 // CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
 // CHECK:STDOUT: target triple = "x86_64-unknown-linux-gnu"
 // CHECK:STDOUT:
-// CHECK:STDOUT: %struct.A = type { i32 }
+// CHECK:STDOUT: %struct.A = type <{ %struct.Base, i32, [4 x i8] }>
+// CHECK:STDOUT: %struct.Base = type { ptr }
 // CHECK:STDOUT:
 // CHECK:STDOUT: $_ZN1A6by_refEv = comdat any
 // CHECK:STDOUT:
@@ -127,13 +145,13 @@ fn Call(n: Cpp.NeedThunk) {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: mustprogress noinline nounwind optnone
-// CHECK:STDOUT: define linkonce_odr dso_local i32 @_ZN1A6by_refEv(ptr nonnull align 4 dereferenceable(4) %this) #0 comdat align 2 {
+// CHECK:STDOUT: define linkonce_odr dso_local i32 @_ZN1A6by_refEv(ptr nonnull align 8 dereferenceable(12) %this) #0 comdat align 2 {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %this.addr = alloca ptr, align 8
 // CHECK:STDOUT:   store ptr %this, ptr %this.addr, align 8
 // CHECK:STDOUT:   %this1 = load ptr, ptr %this.addr, align 8
-// CHECK:STDOUT:   %n = getelementptr inbounds nuw %struct.A, ptr %this1, i32 0, i32 0
-// CHECK:STDOUT:   %0 = load i32, ptr %n, align 4
+// CHECK:STDOUT:   %n = getelementptr inbounds nuw %struct.A, ptr %this1, i32 0, i32 1
+// CHECK:STDOUT:   %0 = load i32, ptr %n, align 8
 // CHECK:STDOUT:   ret i32 %0
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -154,6 +172,45 @@ fn Call(n: Cpp.NeedThunk) {
 // CHECK:STDOUT: !9 = !{}
 // CHECK:STDOUT: !10 = !DILocation(line: 7, column: 10, scope: !7)
 // CHECK:STDOUT: !11 = !DILocation(line: 7, column: 3, scope: !7)
+// CHECK:STDOUT: ; ModuleID = 'call_virtual.carbon'
+// CHECK:STDOUT: source_filename = "call_virtual.carbon"
+// CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+// CHECK:STDOUT: target triple = "x86_64-unknown-linux-gnu"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @_CUseVal.Main(ptr %a) !dbg !7 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc7_4.3.base = getelementptr inbounds nuw [16 x i8], ptr %a, i32 0, i32 0, !dbg !10
+// CHECK:STDOUT:   %Base.virt0.call.vtable = load ptr, ptr %.loc7_4.3.base, align 8, !dbg !10
+// CHECK:STDOUT:   %Base.virt0.call = getelementptr ptr, ptr %Base.virt0.call.vtable, i32 0, !dbg !10
+// CHECK:STDOUT:   %Base.virt0.call.memptr.virtualfn = load ptr, ptr %Base.virt0.call, align 8, !dbg !10
+// CHECK:STDOUT:   call void %Base.virt0.call.memptr.virtualfn(ptr %.loc7_4.3.base), !dbg !10
+// CHECK:STDOUT:   %A.virt1.call.vtable = load ptr, ptr %a, align 8, !dbg !11
+// CHECK:STDOUT:   %A.virt1.call = getelementptr ptr, ptr %A.virt1.call.vtable, i32 1, !dbg !11
+// CHECK:STDOUT:   %A.virt1.call.memptr.virtualfn = load ptr, ptr %A.virt1.call, align 8, !dbg !11
+// CHECK:STDOUT:   call void %A.virt1.call.memptr.virtualfn(ptr %a), !dbg !11
+// CHECK:STDOUT:   ret void, !dbg !12
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_ZN4Base5virt0Ev(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_ZN1A5virt1Ev(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1, !2, !3, !4}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!5}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = !{i32 1, !"wchar_size", i32 4}
+// CHECK:STDOUT: !3 = !{i32 8, !"PIC Level", i32 0}
+// CHECK:STDOUT: !4 = !{i32 7, !"PIE Level", i32 2}
+// CHECK:STDOUT: !5 = distinct !DICompileUnit(language: DW_LANG_C, file: !6, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !6 = !DIFile(filename: "call_virtual.carbon", directory: "")
+// CHECK:STDOUT: !7 = distinct !DISubprogram(name: "UseVal", linkageName: "_CUseVal.Main", scope: null, file: !6, line: 6, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !8 = !DISubroutineType(types: !9)
+// CHECK:STDOUT: !9 = !{}
+// CHECK:STDOUT: !10 = !DILocation(line: 7, column: 3, scope: !7)
+// CHECK:STDOUT: !11 = !DILocation(line: 8, column: 3, scope: !7)
+// CHECK:STDOUT: !12 = !DILocation(line: 6, column: 1, scope: !7)
 // CHECK:STDOUT: ; ModuleID = 'call_thunk.carbon'
 // CHECK:STDOUT: source_filename = "call_thunk.carbon"
 // CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"