Repository: carbon-language/carbon-lang (2026-06-15T18:56:31Z)

Issue:
Crash when calling Carbon function returning a pointer from C++
### Description of the bug:

Doesn't need to be a method, but it does need to be in a derived class.

### What did you do, or what's a simple way to reproduce the bug?

Reproducer:
https://carbon.compiler-explorer.com/z/dEdqv6MGE

### What did you expect to happen?

Compile successfully

### What actually happened?

SIGSEGV

### Any other information, logs, or outputs that you want to share?

```
package Vending library "vending_machine";

// At least one Cpp import is required to use inline C++ code.
// If you're not already importing something, `import Cpp;` will do.
import Cpp library "<iostream>";

class Snack {
  var price: i64;
};

var global_snack: Snack = {.price = 5};

base class VendingMachine {
  // A factory function to initialize the Carbon base class
  fn Create() -> VendingMachine {
    return {} as VendingMachine;
  }

  // Returns a guaranteed non-null pointer to a Snack
  fn DispenseItem[unused self: Self]() -> Snack* {
    return &global_snack;
  }
}

inline Cpp '''
// C++ class extends the Carbon base class
class SnackMachine : public Carbon::VendingMachine {
public:
    SnackMachine() : Carbon::VendingMachine(Carbon::VendingMachine::Create()) {}

    Carbon::Snack* Vend() {
        // Call a method inherited from the Carbon base class.
        // Because Carbon pointers cannot be null, this is guaranteed 
        // to return a valid, non-null Snack pointer.
        Carbon::Snack* retVal = DispenseItem();
        std::cout << "Price = " << retVal->price;
        return retVal;
    }
};

int main() {
    SnackMachine machine;
    Carbon::Snack* my_snack = machine.Vend();
    return 0;
}
''';
```


Discussion:
Somewhat reduced:
```carbon
import Cpp;

class A { var price: i32; };
fn Get() -> A*;

inline Cpp '''
struct S {
  static int Vend() {
    return Carbon::Get()->price;
  }
};

void f() {
  S::Vend();
}
''';
```
It looks to me like this is crashing because the layout of `A` is getting calculated (in `CarbonExternalASTSource::layoutRecordType`) during the lowering phase instead of during the check phase. However, the `CarbonExternalASTSource` is currently only expected to be used during check. It has a `Check::Context*` that won't be valid after check ends. However, `CarbonExternalASTSource` is registered with clang, and clang doesn't know it's invalid later.

At a high level I can imagine two approaches to fixing this:
1. Ensure that the layout gets calculated during check instead of lowering.
2. Make it possible for `layoutRecordType` to work during lowering (perhaps by registering a different external source during the lowering phase?)

I don't have a good intuition as to which approach is better; are there good reasons for layout to sometimes not be needed during check, or is that itself a bug? @zygoloid 
Clang is lazy about computing record layout; in the reduced testcase, the `S::Vend()` construction doesn't need the layout, and in a `-fsyntax-only` compilation, Clang won't compute it. Record layouts are cached, but I don't think that's contractual, so while I think the first approach would work, I'm nervous that it may not be robust in the face of Clang changes. Also, we'll eventually want to support serialization / deserialization of clang ASTs, and I'm pretty sure the record layout cache is *not* serialized, so we'll need something like the second approach for that case anyway.

So yeah, I think we should have a second `ExternalASTSource` that only looks at `SemIR`. We can probably derive the `Check` one from it and share code that way. From a layering perspective this seems like it's a little awkward -- we want this to not be in `check/` to avoid accidental dependence on `Check::Context`, but it seems valuable to keep `sem_ir/` providing only the representation and not operations. Maybe we should split `check/cpp` into a pure-`SemIR` layer (that supports some basic parts of export to Clang AST sufficient for lowering) and a `Check` layer (that does everything else)?



Patch:
diff --git a/MODULE.bazel b/MODULE.bazel
index b5ef04c47d30d..fdfb0dbf27769 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -93,6 +93,7 @@ git_override(
         "//bazel/llvm_project:0004_Introduce_basic_sources_exporting_for_libunwind.patch",
         "//bazel/llvm_project:0005_Introduce_basic_sources_exporting_for_libcxx_and_libcxxabi.patch",
         "//bazel/llvm_project:0009_Introduce_starlark_exporting_compiler-rt_build_information.patch",
+        "//bazel/llvm_project:0011-Add-empty-constructor-and-GetSources-method-to-Multi.patch",
     ],
     remote = "https://github.com/llvm/llvm-project.git",
 )
diff --git a/bazel/llvm_project/0011-Add-empty-constructor-and-GetSources-method-to-Multi.patch b/bazel/llvm_project/0011-Add-empty-constructor-and-GetSources-method-to-Multi.patch
new file mode 100644
index 0000000000000..e98a01b355a4b
--- /dev/null
+++ b/bazel/llvm_project/0011-Add-empty-constructor-and-GetSources-method-to-Multi.patch
@@ -0,0 +1,60 @@
+From 2c7bb43e8800ef29d815f37422a2e56ee0e724b1 Mon Sep 17 00:00:00 2001
+From: Nicholas Bishop <nicholsabishop@google.com>
+Date: Wed, 10 Jun 2026 20:19:57 -0400
+Subject: [PATCH] Add empty constructor and GetSources method to
+ MultiplexExternalSemaSource
+
+---
+ .../include/clang/Sema/MultiplexExternalSemaSource.h  | 11 ++++++++++-
+ clang/lib/Sema/MultiplexExternalSemaSource.cpp        |  3 +++
+ 2 files changed, 13 insertions(+), 1 deletion(-)
+
+diff --git a/clang/include/clang/Sema/MultiplexExternalSemaSource.h b/clang/include/clang/Sema/MultiplexExternalSemaSource.h
+index 12015724b39f..9230d843e923 100644
+--- a/clang/include/clang/Sema/MultiplexExternalSemaSource.h
++++ b/clang/include/clang/Sema/MultiplexExternalSemaSource.h
+@@ -39,10 +39,16 @@ class MultiplexExternalSemaSource : public ExternalSemaSource {
+   /// LLVM-style RTTI.
+   static char ID;
+ 
++public:
++  using SourceVector = SmallVector<llvm::IntrusiveRefCntPtr<ExternalSemaSource>, 2>;
++
+ private:
+-  SmallVector<llvm::IntrusiveRefCntPtr<ExternalSemaSource>, 2> Sources;
++  SourceVector Sources;
+ 
+ public:
++  /// Constructs an empty multiplexing external sema source.
++  MultiplexExternalSemaSource();
++
+   /// Constructs a new multiplexing external sema source and appends the
+   /// given element to it.
+   ///
+@@ -58,6 +64,9 @@ public:
+   ///
+   void AddSource(llvm::IntrusiveRefCntPtr<ExternalSemaSource> Source);
+ 
++  /// Returns a reference to the sources vector.
++  SourceVector& GetSources() { return Sources; }
++
+   //===--------------------------------------------------------------------===//
+   // ExternalASTSource.
+   //===--------------------------------------------------------------------===//
+diff --git a/clang/lib/Sema/MultiplexExternalSemaSource.cpp b/clang/lib/Sema/MultiplexExternalSemaSource.cpp
+index be9582ce501f..4306143025c8 100644
+--- a/clang/lib/Sema/MultiplexExternalSemaSource.cpp
++++ b/clang/lib/Sema/MultiplexExternalSemaSource.cpp
+@@ -16,6 +16,9 @@ using namespace clang;
+ 
+ char MultiplexExternalSemaSource::ID;
+ 
++/// Constructs an empty multiplexing external sema source.
++MultiplexExternalSemaSource::MultiplexExternalSemaSource() {}
++
+ /// Constructs a new multiplexing external sema source and appends the
+ /// given element to it.
+ ///
+-- 
+2.54.0.1136.gdb2ca164c4-goog
+
diff --git a/toolchain/check/BUILD b/toolchain/check/BUILD
index 5898ce637c6d3..5e66adf095183 100644
--- a/toolchain/check/BUILD
+++ b/toolchain/check/BUILD
@@ -186,6 +186,7 @@ cc_library(
         "//toolchain/sem_ir:file",
         "//toolchain/sem_ir:formatter",
         "//toolchain/sem_ir:mangler",
+        "//toolchain/sem_ir:read_only_ast_source",
         "//toolchain/sem_ir:typed_insts",
         "@llvm-project//clang:ast",
         "@llvm-project//clang:basic",
diff --git a/toolchain/check/cpp/export.cpp b/toolchain/check/cpp/export.cpp
index 533548e62e566..7d34eeaa453fd 100644
--- a/toolchain/check/cpp/export.cpp
+++ b/toolchain/check/cpp/export.cpp
@@ -177,39 +177,6 @@ auto ExportClassToCpp(Context& context, SemIR::LocId loc_id,
   return record_decl;
 }
 
-// Get the `StructTypeField`s from a class's object repr.
-static auto GetStructTypeFields(Context& context,
-                                const SemIR::Class& class_info)
-    -> llvm::ArrayRef<SemIR::StructTypeField> {
-  if (class_info.adapt_id.has_value()) {
-    // The representation of an adapter won't necessarily be a
-    // struct. Return an empty array since adapters can't declare
-    // fields.
-    return {};
-  }
-
-  auto object_repr_type_id =
-      class_info.GetObjectRepr(context.sem_ir(), SemIR::SpecificId::None);
-  if (object_repr_type_id == SemIR::ErrorInst::TypeId) {
-    return {};
-  }
-  auto struct_type =
-      context.types().GetAs<SemIR::StructType>(object_repr_type_id);
-  return context.struct_type_fields().Get(struct_type.fields_id);
-}
-
-static auto LookupClassFieldByStructField(
-    const Context& context, const SemIR::NameScope& class_scope,
-    const SemIR::StructTypeField& struct_field)
-    -> std::optional<SemIR::InstStore::GetAsWithIdResult<SemIR::FieldDecl>> {
-  if (auto entry_id = class_scope.Lookup(struct_field.name_id)) {
-    auto field_inst_id =
-        class_scope.GetEntry(*entry_id).result.target_inst_id();
-    return context.insts().TryGetAsWithId<SemIR::FieldDecl>(field_inst_id);
-  }
-  return std::nullopt;
-}
-
 static auto SetCppClassMemberAccess(const SemIR::NameScope& class_scope,
                                     SemIR::NameId member_name_id,
                                     clang::Decl* member) -> void {
@@ -265,9 +232,10 @@ auto ExportAllFieldsToCpp(Context& context, SemIR::Class& class_info) -> void {
 
   const auto& class_scope = context.name_scopes().Get(class_info.scope_id);
 
-  for (const auto& struct_field : GetStructTypeFields(context, class_info)) {
-    auto class_field =
-        LookupClassFieldByStructField(context, class_scope, struct_field);
+  for (const auto& struct_field :
+       class_info.GetStructTypeFields(context.sem_ir())) {
+    auto class_field = LookupClassFieldByStructField(context.sem_ir(),
+                                                     class_scope, struct_field);
     if (!class_field) {
       continue;
     }
@@ -314,42 +282,6 @@ auto ExportFieldToCpp(Context& context, SemIR::InstId field_inst_id,
   return nullptr;
 }
 
-auto CalculateCppFieldOffsets(
-    Context& context, SemIR::ClassId class_id,
-    llvm::DenseMap<const clang::FieldDecl*, uint64_t>& field_offsets) -> bool {
-  auto class_info = context.classes().Get(class_id);
-  const auto& class_scope = context.name_scopes().Get(class_info.scope_id);
-
-  auto class_layout = SemIR::ObjectLayout::Empty();
-  for (const auto& struct_field : GetStructTypeFields(context, class_info)) {
-    auto field_type_id = context.sem_ir().types().GetTypeIdForTypeInstId(
-        struct_field.type_inst_id);
-    auto field_layout = context.sem_ir()
-                            .types()
-                            .GetCompleteTypeInfo(field_type_id)
-                            .object_layout;
-
-    // Use the field's name to look up the corresponding entry in the
-    // class. If it's a `FieldDecl`, write out the offset of the
-    // corresponding `clang::FieldDecl`.
-    auto class_field =
-        LookupClassFieldByStructField(context, class_scope, struct_field);
-    if (class_field) {
-      auto* cpp_field_decl =
-          ExportFieldToCpp(context, class_field->inst_id, class_field->inst);
-      if (!cpp_field_decl) {
-        return false;
-      }
-      field_offsets.insert(
-          {cpp_field_decl, class_layout.FieldOffset(field_layout).bits()});
-    }
-
-    class_layout.AppendField(field_layout);
-  }
-
-  return true;
-}
-
 namespace {
 struct FunctionInfo {
   struct Param {
diff --git a/toolchain/check/cpp/export.h b/toolchain/check/cpp/export.h
index 0e6e95b9b8a26..1311d7d2c7e7e 100644
--- a/toolchain/check/cpp/export.h
+++ b/toolchain/check/cpp/export.h
@@ -51,13 +51,6 @@ auto ExportAllFieldsToCpp(Context& context, SemIR::Class& class_info) -> void;
 auto ExportFieldToCpp(Context& context, SemIR::InstId field_inst_id,
                       SemIR::FieldDecl field_decl) -> clang::FieldDecl*;
 
-// Get the field offset for each field in a class.
-//
-// Returns true on success, false if any error occurs.
-auto CalculateCppFieldOffsets(
-    Context& context, SemIR::ClassId class_id,
-    llvm::DenseMap<const clang::FieldDecl*, uint64_t>& field_offsets) -> bool;
-
 // Get a `clang::FunctionDecl` that can be used to call a Carbon function.
 auto ExportFunctionToCpp(Context& context, SemIR::LocId loc_id,
                          SemIR::FunctionId function_id) -> clang::FunctionDecl*;
diff --git a/toolchain/check/cpp/generate_ast.cpp b/toolchain/check/cpp/generate_ast.cpp
index 5737d203ffa99..4b369a07a7a7e 100644
--- a/toolchain/check/cpp/generate_ast.cpp
+++ b/toolchain/check/cpp/generate_ast.cpp
@@ -24,6 +24,7 @@
 #include "common/map.h"
 #include "common/raw_string_ostream.h"
 #include "llvm/ADT/IntrusiveRefCntPtr.h"
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/raw_ostream.h"
 #include "toolchain/base/kind_switch.h"
@@ -41,6 +42,7 @@
 #include "toolchain/diagnostics/format_providers.h"
 #include "toolchain/parse/node_ids.h"
 #include "toolchain/sem_ir/cpp_file.h"
+#include "toolchain/sem_ir/read_only_ast_source.h"
 #include "toolchain/sem_ir/typed_insts.h"
 
 namespace Carbon::Check {
@@ -323,9 +325,10 @@ class ShallowCopyCompilerInvocation : public clang::CompilerInvocation {
 };
 
 // Provides clang AST nodes representing Carbon SemIR entities.
-class CarbonExternalASTSource : public clang::ExternalSemaSource {
+class CarbonExternalASTSource : public SemIR::ReadOnlyASTSource {
  public:
-  explicit CarbonExternalASTSource(Context* context) : context_(context) {}
+  explicit CarbonExternalASTSource(Context* context)
+      : ReadOnlyASTSource(context->sem_ir()), context_(context) {}
 
   auto StartTranslationUnit(clang::ASTConsumer* consumer) -> void override;
 
@@ -345,6 +348,13 @@ class CarbonExternalASTSource : public clang::ExternalSemaSource {
       llvm::DenseMap<const clang::CXXRecordDecl*, clang::CharUnits>&
           vbase_offsets) -> bool override;
 
+  auto isA(const void* class_id) const -> bool override {
+    return class_id == &id || ReadOnlyASTSource::isA(class_id);
+  }
+  static auto classof(const ExternalASTSource* s) -> bool {
+    return s->isA(&id);
+  }
+
  private:
   // Builds the top-level C++ namespace `Carbon` and adds it to the translation
   // unit.
@@ -380,9 +390,14 @@ class CarbonExternalASTSource : public clang::ExternalSemaSource {
         SemIR::ImportIRInst(clang_source_loc_id));
   }
 
+  // For LLVM RTTI.
+  static char id;
+
   Check::Context* context_;
 };
 
+char CarbonExternalASTSource::id;
+
 }  // namespace
 
 void CarbonExternalASTSource::StartTranslationUnit(
@@ -575,54 +590,6 @@ auto CarbonExternalASTSource::FindExternalVisibleDeclsByName(
   }
 }
 
-// If this declaration declares a class type that is "owned" by Carbon, and not
-// imported from C++, returns the corresponding type ID and `ClassType`.
-// Otherwise returns `nullopt`.
-static auto GetAsCarbonOwnedClass(Context& context,
-                                  const clang::TagDecl* tag_decl)
-    -> std::optional<std::pair<SemIR::TypeId, SemIR::ClassType>> {
-  // Quickly check whether we could possibly own this class.
-  // TODO: Once we multiplex with the ASTReader, handle
-  // ASTReader::completeVisibleDeclsMap setting this to `false`.
-  if (!tag_decl->hasExternalVisibleStorage()) {
-    return std::nullopt;
-  }
-
-  auto key = SemIR::ClangDeclKey::ForNonFunctionDecl(
-      const_cast<clang::TagDecl*>(tag_decl->getFirstDecl()));
-  auto clang_decl_id = context.clang_decls().LookupId(key);
-  if (!clang_decl_id.has_value()) {
-    return std::nullopt;
-  }
-
-  auto inst_id = context.clang_decls().Get(clang_decl_id).inst_id;
-  auto const_id = context.constant_values().Get(inst_id);
-  if (!const_id.has_value()) {
-    return std::nullopt;
-  }
-
-  auto class_type =
-      context.constant_values().TryGetInstAs<SemIR::ClassType>(const_id);
-  if (!class_type) {
-    return std::nullopt;
-  }
-
-  // Determine whether this class was imported from C++.
-  // TODO: This currently can't happen, because only Carbon classes have
-  // external lexical storage, but will happen once we support importing C++
-  // classes from AST files. Add a test once that is supported.
-  // TODO: Consider setting `extern_library_id` on classes imported from C++ to
-  // indicate the current file does not own them.
-  const auto& class_info = context.classes().Get(class_type->class_id);
-  if (class_info.parent_scope_id.has_value() &&
-      context.name_scopes().Get(class_info.parent_scope_id).is_cpp_scope()) {
-    return std::nullopt;
-  }
-
-  auto class_type_id = context.types().GetTypeIdForTypeConstantId(const_id);
-  return std::make_pair(class_type_id, *class_type);
-}
-
 auto CarbonExternalASTSource::CompleteType(clang::TagDecl* tag_decl) -> void {
   auto* class_decl = dyn_cast<clang::CXXRecordDecl>(tag_decl);
   if (!class_decl) {
@@ -631,7 +598,8 @@ auto CarbonExternalASTSource::CompleteType(clang::TagDecl* tag_decl) -> void {
     return;
   }
 
-  auto carbon_class_info = GetAsCarbonOwnedClass(*context_, tag_decl);
+  auto carbon_class_info =
+      SemIR::GetAsCarbonOwnedClass(context_->sem_ir(), tag_decl);
   if (!carbon_class_info) {
     return;
   }
@@ -736,7 +704,8 @@ auto CarbonExternalASTSource::layoutRecordType(
     llvm::DenseMap<const clang::CXXRecordDecl*, clang::CharUnits>& base_offsets,
     llvm::DenseMap<const clang::CXXRecordDecl*, clang::CharUnits>&
         vbase_offsets) -> bool {
-  auto carbon_class_info = GetAsCarbonOwnedClass(*context_, record_decl);
+  auto carbon_class_info =
+      SemIR::GetAsCarbonOwnedClass(context_->sem_ir(), record_decl);
   if (!carbon_class_info) {
     return false;
   }
@@ -748,37 +717,11 @@ auto CarbonExternalASTSource::layoutRecordType(
   // general.
   CompleteTypeOrCheckFail(*context_, class_type_id);
 
-  // Set the overall size and alignment. We round up the size to an integer
-  // number of bytes in order to avoid surprising Clang too much.
-  auto layout = context_->sem_ir()
-                    .types()
-                    .GetCompleteTypeInfo(class_type_id)
-                    .object_layout;
-  size = layout.size.bytes() * 8;
-  alignment = layout.alignment.bits();
-
-  // Fill in `field_offsets`.
-  CalculateCppFieldOffsets(*context_, class_type.class_id, field_offsets);
-
-  // Add offset for base class, if any.
-  if (const auto* class_decl = dyn_cast<clang::CXXRecordDecl>(record_decl);
-      class_decl && !class_decl->bases().empty()) {
-    CARBON_CHECK(class_decl->getNumBases() == 1,
-                 "Carbon class with multiple bases");
-    const auto& base = *class_decl->bases_begin();
-    // TODO: If this class introduced a vptr, the base will be at an offset of
-    // `sizeof(void*)`, not 0.
-    base_offsets.insert(
-        {base.getType()->getAsCXXRecordDecl()->getCanonicalDecl(),
-         clang::CharUnits::Zero()});
-
-    // TODO: Support deriving from a C++ class with virtual bases.
-    CARBON_CHECK(class_decl->getNumVBases() == 0,
-                 "Carbon class with multiple bases");
-    static_cast<void>(vbase_offsets);
-  }
+  auto& class_info = context_->classes().Get(class_type.class_id);
+  ExportAllFieldsToCpp(*context_, class_info);
 
-  return true;
+  return ReadOnlyASTSource::layoutRecordType(
+      record_decl, size, alignment, field_offsets, base_offsets, vbase_offsets);
 }
 
 // Parses a sequence of top-level declarations and forms a corresponding
@@ -945,17 +888,26 @@ auto GenerateAst(Context& context,
   context.sem_ir().cpp_file()->CreateMangleContext();
 
   auto& ast = clang_instance.getASTContext();
-  llvm::IntrusiveRefCntPtr<clang::ExternalSemaSource> carbon_source =
-      llvm::makeIntrusiveRefCnt<CarbonExternalASTSource>(&context);
+
+  // Always build a multiplex source, even if there's only one child
+  // source. During lowering, the `CarbonExternalASTSource` can no longer be
+  // used (because it uses `Check::Context`), so a `ReadOnlyASTSource` is
+  // installed instead. However, clang internally keeps pointers to the
+  // top-level `ExternalASTSource` installed via `setExternalSource`, and
+  // those pointers aren't updated if `setExternalSource` is called again. By
+  // using `MultiplexExternalSemaSource`, we can keep the top-level
+  // `ExternalASTSource` pointer the same, and only update its children.
+  auto multiplex_source_ref_cnt_ptr =
+      llvm::makeIntrusiveRefCnt<clang::MultiplexExternalSemaSource>();
+  auto* multiplex_source = cast<clang::MultiplexExternalSemaSource>(
+      multiplex_source_ref_cnt_ptr.get());
   if (auto* existing_source = llvm::cast_or_null<clang::ExternalSemaSource>(
           ast.getExternalSource())) {
-    auto multiplex_source =
-        llvm::makeIntrusiveRefCnt<clang::MultiplexExternalSemaSource>(
-            existing_source, std::move(carbon_source));
-    ast.setExternalSource(std::move(multiplex_source));
-  } else {
-    ast.setExternalSource(std::move(carbon_source));
+    multiplex_source->AddSource(existing_source);
   }
+  multiplex_source->AddSource(
+      llvm::makeIntrusiveRefCnt<CarbonExternalASTSource>(&context));
+  ast.setExternalSource(std::move(multiplex_source_ref_cnt_ptr));
 
   if (llvm::Error error = action.Execute()) {
     // `Execute` currently never fails, but its contract allows it to.
@@ -1008,6 +960,21 @@ auto FinishAst(Context& context) -> void {
 
   context.cpp_context()->sema().ActOnEndOfTranslationUnit();
 
+  // Remove the `CarbonExternalASTSource` installed in `GenerateAst` and
+  // replace it with a `ReadOnlyASTSource`. This is necessary because
+  // the source may be accessed later during lowering, but the
+  // `CarbonExternalASTSource` has a pointer to `Check::Context` that
+  // will not remain valid.
+  auto* multiplex_source = cast<clang::MultiplexExternalSemaSource>(
+      context.ast_context().getExternalSource());
+  auto& child_sources = multiplex_source->GetSources();
+  llvm::erase_if(child_sources, [](const auto& src) {
+    // `CarbonExternalASTSource` inherits from `ReadOnlyASTSource`.
+    return llvm::isa<SemIR::ReadOnlyASTSource>(src.get());
+  });
+  multiplex_source->AddSource(
+      llvm::makeIntrusiveRefCnt<SemIR::ReadOnlyASTSource>(context.sem_ir()));
+
   // We don't call FrontendAction::EndSourceFile, because that destroys the AST.
   context.set_cpp_context(nullptr);
 
diff --git a/toolchain/sem_ir/BUILD b/toolchain/sem_ir/BUILD
index 378bdceee0d49..aba7c5053ddc3 100644
--- a/toolchain/sem_ir/BUILD
+++ b/toolchain/sem_ir/BUILD
@@ -382,3 +382,13 @@ cc_library(
         "@llvm-project//llvm:Support",
     ],
 )
+
+cc_library(
+    name = "read_only_ast_source",
+    srcs = ["read_only_ast_source.cpp"],
+    hdrs = ["read_only_ast_source.h"],
+    deps = [
+        "//toolchain/sem_ir:file",
+        "@llvm-project//clang:sema",
+    ],
+)
diff --git a/toolchain/sem_ir/class.cpp b/toolchain/sem_ir/class.cpp
index dfa0dc66bfd55..0eb072dc9ffaa 100644
--- a/toolchain/sem_ir/class.cpp
+++ b/toolchain/sem_ir/class.cpp
@@ -4,6 +4,7 @@
 
 #include "toolchain/sem_ir/class.h"
 
+#include "clang/AST/Decl.h"
 #include "toolchain/base/value_store_impl.h"
 #include "toolchain/sem_ir/file.h"
 #include "toolchain/sem_ir/generic.h"
@@ -51,6 +52,80 @@ auto Class::GetObjectRepr(const File& file, SpecificId specific_id) const
           .object_repr_type_inst_id);
 }
 
+auto Class::GetStructTypeFields(const File& sem_ir) const
+    -> llvm::ArrayRef<SemIR::StructTypeField> {
+  if (adapt_id.has_value()) {
+    // The representation of an adapter won't necessarily be a
+    // struct. Return an empty array since adapters can't declare
+    // fields.
+    return {};
+  }
+
+  auto object_repr_type_id = GetObjectRepr(sem_ir, SemIR::SpecificId::None);
+  if (object_repr_type_id == SemIR::ErrorInst::TypeId) {
+    return {};
+  }
+  auto struct_type =
+      sem_ir.types().GetAs<SemIR::StructType>(object_repr_type_id);
+  return sem_ir.struct_type_fields().Get(struct_type.fields_id);
+}
+
+auto GetAsCarbonOwnedClass(const File& sem_ir, const clang::TagDecl* tag_decl)
+    -> std::optional<std::pair<SemIR::TypeId, SemIR::ClassType>> {
+  // Quickly check whether we could possibly own this class.
+  // TODO: Once we multiplex with the ASTReader, handle
+  // ASTReader::completeVisibleDeclsMap setting this to `false`.
+  if (!tag_decl->hasExternalVisibleStorage()) {
+    return std::nullopt;
+  }
+
+  auto key = SemIR::ClangDeclKey::ForNonFunctionDecl(
+      const_cast<clang::TagDecl*>(tag_decl->getFirstDecl()));
+  auto clang_decl_id = sem_ir.clang_decls().LookupId(key);
+  if (!clang_decl_id.has_value()) {
+    return std::nullopt;
+  }
+
+  auto inst_id = sem_ir.clang_decls().Get(clang_decl_id).inst_id;
+  auto const_id = sem_ir.constant_values().Get(inst_id);
+  if (!const_id.has_value()) {
+    return std::nullopt;
+  }
+
+  auto class_type =
+      sem_ir.constant_values().TryGetInstAs<SemIR::ClassType>(const_id);
+  if (!class_type) {
+    return std::nullopt;
+  }
+
+  // Determine whether this class was imported from C++.
+  // TODO: This currently can't happen, because only Carbon classes have
+  // external lexical storage, but will happen once we support importing C++
+  // classes from AST files. Add a test once that is supported.
+  // TODO: Consider setting `extern_library_id` on classes imported from C++ to
+  // indicate the current file does not own them.
+  const auto& class_info = sem_ir.classes().Get(class_type->class_id);
+  if (class_info.parent_scope_id.has_value() &&
+      sem_ir.name_scopes().Get(class_info.parent_scope_id).is_cpp_scope()) {
+    return std::nullopt;
+  }
+
+  auto class_type_id = sem_ir.types().GetTypeIdForTypeConstantId(const_id);
+  return std::make_pair(class_type_id, *class_type);
+}
+
+auto LookupClassFieldByStructField(const File& sem_ir,
+                                   const SemIR::NameScope& class_scope,
+                                   const SemIR::StructTypeField& struct_field)
+    -> std::optional<SemIR::InstStore::GetAsWithIdResult<SemIR::FieldDecl>> {
+  if (auto entry_id = class_scope.Lookup(struct_field.name_id)) {
+    auto field_inst_id =
+        class_scope.GetEntry(*entry_id).result.target_inst_id();
+    return sem_ir.insts().TryGetAsWithId<SemIR::FieldDecl>(field_inst_id);
+  }
+  return std::nullopt;
+}
+
 }  // namespace Carbon::SemIR
 
 namespace Carbon {
diff --git a/toolchain/sem_ir/class.h b/toolchain/sem_ir/class.h
index 1c6b5b7fdb3e2..476d884b861c1 100644
--- a/toolchain/sem_ir/class.h
+++ b/toolchain/sem_ir/class.h
@@ -5,13 +5,24 @@
 #ifndef CARBON_TOOLCHAIN_SEM_IR_CLASS_H_
 #define CARBON_TOOLCHAIN_SEM_IR_CLASS_H_
 
+#include <optional>
+
 #include "common/map.h"
 #include "toolchain/base/value_store.h"
 #include "toolchain/sem_ir/entity_with_params_base.h"
 #include "toolchain/sem_ir/ids.h"
+#include "toolchain/sem_ir/inst.h"
+#include "toolchain/sem_ir/struct_type_field.h"
+
+namespace clang {
+class TagDecl;
+}
 
 namespace Carbon::SemIR {
 
+class File;
+class NameScope;
+
 // Class-specific fields.
 struct ClassFields {
   enum InheritanceKind : int8_t {
@@ -129,10 +140,25 @@ struct Class : public EntityWithParamsBase,
   // Gets the object representation for this class. Returns `None` if the class
   // is not yet defined.
   auto GetObjectRepr(const File& file, SpecificId specific_id) const -> TypeId;
+
+  // Get the `StructTypeField`s from a class's object repr.
+  auto GetStructTypeFields(const File& sem_ir) const
+      -> llvm::ArrayRef<SemIR::StructTypeField>;
 };
 
 using ClassStore = ValueStore<ClassId, Class, Tag<CheckIRId>>;
 
+// If this declaration declares a class type that is "owned" by Carbon, and not
+// imported from C++, returns the corresponding type ID and `ClassType`.
+// Otherwise returns `nullopt`.
+auto GetAsCarbonOwnedClass(const File& sem_ir, const clang::TagDecl* tag_decl)
+    -> std::optional<std::pair<SemIR::TypeId, SemIR::ClassType>>;
+
+auto LookupClassFieldByStructField(const File& sem_ir,
+                                   const NameScope& class_scope,
+                                   const StructTypeField& struct_field)
+    -> std::optional<InstStore::GetAsWithIdResult<SemIR::FieldDecl>>;
+
 }  // namespace Carbon::SemIR
 
 namespace Carbon {
diff --git a/toolchain/sem_ir/read_only_ast_source.cpp b/toolchain/sem_ir/read_only_ast_source.cpp
new file mode 100644
index 0000000000000..fd0eacb876551
--- /dev/null
+++ b/toolchain/sem_ir/read_only_ast_source.cpp
@@ -0,0 +1,93 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "toolchain/sem_ir/read_only_ast_source.h"
+
+namespace Carbon::SemIR {
+
+char ReadOnlyASTSource::id;
+
+// Get the field offset for each field in a class.
+//
+// Returns true on success, false if any error occurs.
+static auto CalculateCppFieldOffsets(
+    const File& sem_ir, SemIR::ClassId class_id,
+    llvm::DenseMap<const clang::FieldDecl*, uint64_t>& field_offsets) -> bool {
+  auto class_info = sem_ir.classes().Get(class_id);
+  const auto& class_scope = sem_ir.name_scopes().Get(class_info.scope_id);
+
+  auto class_layout = SemIR::ObjectLayout::Empty();
+  for (const auto& struct_field : class_info.GetStructTypeFields(sem_ir)) {
+    auto field_type_id =
+        sem_ir.types().GetTypeIdForTypeInstId(struct_field.type_inst_id);
+    auto field_layout =
+        sem_ir.types().GetCompleteTypeInfo(field_type_id).object_layout;
+
+    // Use the field's name to look up the corresponding entry in the
+    // class. If it's a `FieldDecl`, write out the offset of the
+    // corresponding `clang::FieldDecl`.
+    auto class_field =
+        LookupClassFieldByStructField(sem_ir, class_scope, struct_field);
+    if (class_field) {
+      const auto* clang_decl =
+          sem_ir.clang_decls().Lookup(class_field->inst_id);
+      if (!clang_decl) {
+        return false;
+      }
+
+      auto* cpp_field_decl = cast<clang::FieldDecl>(clang_decl->decl());
+      field_offsets.insert(
+          {cpp_field_decl, class_layout.FieldOffset(field_layout).bits()});
+    }
+
+    class_layout.AppendField(field_layout);
+  }
+
+  return true;
+}
+
+auto ReadOnlyASTSource::layoutRecordType(
+    const clang::RecordDecl* record_decl, uint64_t& size, uint64_t& alignment,
+    llvm::DenseMap<const clang::FieldDecl*, uint64_t>& field_offsets,
+    llvm::DenseMap<const clang::CXXRecordDecl*, clang::CharUnits>& base_offsets,
+    llvm::DenseMap<const clang::CXXRecordDecl*, clang::CharUnits>&
+        vbase_offsets) -> bool {
+  auto carbon_class_info = GetAsCarbonOwnedClass(sem_ir_, record_decl);
+  if (!carbon_class_info) {
+    return false;
+  }
+  auto& [class_type_id, class_type] = *carbon_class_info;
+
+  // Set the overall size and alignment. We round up the size to an integer
+  // number of bytes in order to avoid surprising Clang too much.
+  auto layout =
+      sem_ir_.types().GetCompleteTypeInfo(class_type_id).object_layout;
+  size = layout.size.bytes() * 8;
+  alignment = layout.alignment.bits();
+
+  // Fill in `field_offsets`.
+  CalculateCppFieldOffsets(sem_ir_, class_type.class_id, field_offsets);
+
+  // Add offset for base class, if any.
+  if (const auto* class_decl = dyn_cast<clang::CXXRecordDecl>(record_decl);
+      class_decl && !class_decl->bases().empty()) {
+    CARBON_CHECK(class_decl->getNumBases() == 1,
+                 "Carbon class with multiple bases");
+    const auto& base = *class_decl->bases_begin();
+    // TODO: If this class introduced a vptr, the base will be at an offset of
+    // `sizeof(void*)`, not 0.
+    base_offsets.insert(
+        {base.getType()->getAsCXXRecordDecl()->getCanonicalDecl(),
+         clang::CharUnits::Zero()});
+
+    // TODO: Support deriving from a C++ class with virtual bases.
+    CARBON_CHECK(class_decl->getNumVBases() == 0,
+                 "Carbon class with multiple bases");
+    static_cast<void>(vbase_offsets);
+  }
+
+  return true;
+}
+
+}  // namespace Carbon::SemIR
diff --git a/toolchain/sem_ir/read_only_ast_source.h b/toolchain/sem_ir/read_only_ast_source.h
new file mode 100644
index 0000000000000..2a22a8b788363
--- /dev/null
+++ b/toolchain/sem_ir/read_only_ast_source.h
@@ -0,0 +1,41 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CARBON_TOOLCHAIN_SEM_IR_READ_ONLY_AST_SOURCE_H_
+#define CARBON_TOOLCHAIN_SEM_IR_READ_ONLY_AST_SOURCE_H_
+
+#include "clang/Sema/ExternalSemaSource.h"
+#include "toolchain/sem_ir/file.h"
+
+namespace Carbon::SemIR {
+
+class ReadOnlyASTSource : public clang::ExternalSemaSource {
+ public:
+  explicit ReadOnlyASTSource(const File& sem_ir) : sem_ir_(sem_ir) {}
+
+  auto layoutRecordType(
+      const clang::RecordDecl* record_decl, uint64_t& size, uint64_t& alignment,
+      llvm::DenseMap<const clang::FieldDecl*, uint64_t>& field_offsets,
+      llvm::DenseMap<const clang::CXXRecordDecl*, clang::CharUnits>&
+          base_offsets,
+      llvm::DenseMap<const clang::CXXRecordDecl*, clang::CharUnits>&
+          vbase_offsets) -> bool override;
+
+  auto isA(const void* class_id) const -> bool override {
+    return class_id == &id || ExternalSemaSource::isA(class_id);
+  }
+  static auto classof(const ExternalASTSource* s) -> bool {
+    return s->isA(&id);
+  }
+
+ private:
+  // For LLVM RTTI.
+  static char id;
+
+  const File& sem_ir_;
+};
+
+}  // namespace Carbon::SemIR
+
+#endif  // CARBON_TOOLCHAIN_SEM_IR_READ_ONLY_AST_SOURCE_H_


Test patch:
diff --git a/toolchain/lower/testdata/interop/cpp/issue7142.carbon b/toolchain/lower/testdata/interop/cpp/issue7142.carbon
new file mode 100644
index 0000000000000..e8b40eeb8cf5f
--- /dev/null
+++ b/toolchain/lower/testdata/interop/cpp/issue7142.carbon
@@ -0,0 +1,146 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lower/testdata/interop/cpp/issue7142.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/interop/cpp/issue7142.carbon
+
+import Cpp;
+
+class A { var price: i32; };
+fn Get() -> A*;
+
+inline Cpp '''
+struct S {
+  static int Vend() {
+    return Carbon::Get()->price;
+  }
+};
+
+void f() {
+  S::Vend();
+}
+''';
+
+// CHECK:STDOUT: ; ModuleID = 'issue7142.carbon'
+// CHECK:STDOUT: source_filename = "issue7142.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: %"class.Carbon::A" = type { i32 }
+// CHECK:STDOUT:
+// CHECK:STDOUT: $_ZN1S4VendEv = comdat any
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: mustprogress uwtable
+// CHECK:STDOUT: define dso_local void @_Z1fv() #0 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %call = call noundef i32 @_ZN1S4VendEv()
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: mustprogress uwtable
+// CHECK:STDOUT: define linkonce_odr dso_local noundef i32 @_ZN1S4VendEv() #0 comdat align 2 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %call = call noundef ptr @_ZN6CarbonL3GetEv()
+// CHECK:STDOUT:   %price = getelementptr inbounds nuw %"class.Carbon::A", ptr %call, i32 0, i32 0
+// CHECK:STDOUT:   %0 = load i32, ptr %price, align 4, !tbaa !11
+// CHECK:STDOUT:   ret i32 %0
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare ptr @_CGet.Main()
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CGet__carbon_thunk.Main(ptr %_) #1 !dbg !13 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %Get.call = call ptr @_CGet.Main(), !dbg !19
+// CHECK:STDOUT:   store ptr %Get.call, ptr %_, align 8, !dbg !19
+// CHECK:STDOUT:   ret void, !dbg !19
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline nounwind
+// CHECK:STDOUT: define void @"_C__destroy_thunk:thunk.A.Main"(ptr %self) #2 !dbg !20 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   call void @"_COp.68011419f123494e:core.Destroy.Core"(ptr %self), !dbg !23
+// CHECK:STDOUT:   ret void, !dbg !23
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.5e27612b9dd31a14:core.Destroy.Core"(ptr %self) #1 !dbg !24 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !30
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.dd803598fd1f0f08:core.Destroy.Core"(ptr %self) #1 !dbg !31 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !34
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.68011419f123494e:core.Destroy.Core"(ptr %self) #1 !dbg !35 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !38
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress nounwind uwtable
+// CHECK:STDOUT: define internal noundef ptr @_ZN6CarbonL3GetEv() #3 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %retval = alloca ptr, align 8
+// CHECK:STDOUT:   call void @_CGet__carbon_thunk.Main(ptr noundef nonnull align 8 dereferenceable(8) %retval)
+// CHECK:STDOUT:   %0 = load ptr, ptr %retval, align 8
+// CHECK:STDOUT:   ret ptr %0
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { mustprogress uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+// CHECK:STDOUT: attributes #1 = { nounwind }
+// CHECK:STDOUT: attributes #2 = { alwaysinline nounwind }
+// CHECK:STDOUT: attributes #3 = { alwaysinline mustprogress nounwind uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1, !2, !3, !4}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!5}
+// CHECK:STDOUT: !llvm.errno.tbaa = !{!7}
+// 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 8, !"PIC Level", i32 2}
+// CHECK:STDOUT: !3 = !{i32 7, !"PIE Level", i32 2}
+// CHECK:STDOUT: !4 = !{i32 7, !"uwtable", i32 2}
+// CHECK:STDOUT: !5 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !6, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !6 = !DIFile(filename: "issue7142.carbon", directory: "")
+// CHECK:STDOUT: !7 = !{!8, !8, i64 0}
+// CHECK:STDOUT: !8 = !{!"int", !9, i64 0}
+// CHECK:STDOUT: !9 = !{!"omnipotent char", !10, i64 0}
+// CHECK:STDOUT: !10 = !{!"Simple C++ TBAA"}
+// CHECK:STDOUT: !11 = !{!12, !8, i64 0}
+// CHECK:STDOUT: !12 = !{!"_ZTSN6Carbon1AE", !8, i64 0}
+// CHECK:STDOUT: !13 = distinct !DISubprogram(name: "Get__carbon_thunk", linkageName: "_CGet__carbon_thunk.Main", scope: null, file: !6, line: 16, type: !14, spFlags: DISPFlagDefinition, unit: !5, retainedNodes: !17)
+// CHECK:STDOUT: !14 = !DISubroutineType(types: !15)
+// CHECK:STDOUT: !15 = !{null, !16}
+// CHECK:STDOUT: !16 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)
+// CHECK:STDOUT: !17 = !{!18}
+// CHECK:STDOUT: !18 = !DILocalVariable(arg: 1, scope: !13, type: !16)
+// CHECK:STDOUT: !19 = !DILocation(line: 16, column: 1, scope: !13)
+// CHECK:STDOUT: !20 = distinct !DISubprogram(name: "__destroy_thunk", linkageName: "_C__destroy_thunk:thunk.A.Main", scope: null, file: !6, line: 15, type: !14, spFlags: DISPFlagDefinition, unit: !5, retainedNodes: !21)
+// CHECK:STDOUT: !21 = !{!22}
+// CHECK:STDOUT: !22 = !DILocalVariable(arg: 1, scope: !20, type: !16)
+// CHECK:STDOUT: !23 = !DILocation(line: 15, column: 1, scope: !20)
+// CHECK:STDOUT: !24 = distinct !DISubprogram(name: "Op", linkageName: "_COp.5e27612b9dd31a14:core.Destroy.Core", scope: null, file: !6, line: 15, type: !25, spFlags: DISPFlagDefinition, unit: !5, retainedNodes: !28)
+// CHECK:STDOUT: !25 = !DISubroutineType(types: !26)
+// CHECK:STDOUT: !26 = !{null, !27}
+// CHECK:STDOUT: !27 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+// CHECK:STDOUT: !28 = !{!29}
+// CHECK:STDOUT: !29 = !DILocalVariable(arg: 1, scope: !24, type: !27)
+// CHECK:STDOUT: !30 = !DILocation(line: 15, column: 1, scope: !24)
+// CHECK:STDOUT: !31 = distinct !DISubprogram(name: "Op", linkageName: "_COp.dd803598fd1f0f08:core.Destroy.Core", scope: null, file: !6, line: 15, type: !14, spFlags: DISPFlagDefinition, unit: !5, retainedNodes: !32)
+// CHECK:STDOUT: !32 = !{!33}
+// CHECK:STDOUT: !33 = !DILocalVariable(arg: 1, scope: !31, type: !16)
+// CHECK:STDOUT: !34 = !DILocation(line: 15, column: 1, scope: !31)
+// CHECK:STDOUT: !35 = distinct !DISubprogram(name: "Op", linkageName: "_COp.68011419f123494e:core.Destroy.Core", scope: null, file: !6, line: 15, type: !14, spFlags: DISPFlagDefinition, unit: !5, retainedNodes: !36)
+// CHECK:STDOUT: !36 = !{!37}
+// CHECK:STDOUT: !37 = !DILocalVariable(arg: 1, scope: !35, type: !16)
+// CHECK:STDOUT: !38 = !DILocation(line: 15, column: 1, scope: !35)