Make items that stem from decl context have a list of children item ids and generate code recursively

After this cl:
  * We use a new method `Importer::GetItemIdsInSourceOrder()` to obtain a list of their children items (including comments and unsupported items, excluding children `decl`s whose `ImportDecl()` returned `std::nullopt`, as they don't have a corresponding item.)
  * We generate source for items recursively, starting from `flat_ir.top_level_item_ids`.
    * This required some changes to `generate_record()`, as it used to not be responsible for generating (unsupported) items for its own children, and now it is.
  * Comments: We store all the comments, so we can find the relevant comments for each decl context.
    * We now generate free comments that appear within a `Record`; however, comments between fields now appear out of place, after the struct definition. I will address this in a followup cl.

PiperOrigin-RevId: 440906345
diff --git a/rs_bindings_from_cc/importer.cc b/rs_bindings_from_cc/importer.cc
index c06fcbf..2a50fba 100644
--- a/rs_bindings_from_cc/importer.cc
+++ b/rs_bindings_from_cc/importer.cc
@@ -48,6 +48,7 @@
 #include "third_party/llvm/llvm-project/clang/include/clang/Basic/Specifiers.h"
 #include "third_party/llvm/llvm-project/clang/include/clang/Sema/Sema.h"
 #include "third_party/llvm/llvm-project/llvm/include/llvm/ADT/Optional.h"
+#include "third_party/llvm/llvm-project/llvm/include/llvm/ADT/STLExtras.h"
 #include "third_party/llvm/llvm-project/llvm/include/llvm/ADT/SmallPtrSet.h"
 #include "third_party/llvm/llvm-project/llvm/include/llvm/Support/Casting.h"
 #include "third_party/llvm/llvm-project/llvm/include/llvm/Support/ErrorHandling.h"
@@ -188,54 +189,6 @@
 
 }  // namespace
 
-std::vector<clang::RawComment*> Importer::ImportFreeComments() {
-  clang::SourceManager& sm = ctx_.getSourceManager();
-
-  // We put all comments into an ordered set in source order. Later we'll remove
-  // the comments that we don't want or that we get by other means.
-  auto source_order = [&sm](const clang::SourceLocation& a,
-                            const clang::SourceLocation& b) {
-    return b.isValid() && (a.isInvalid() || sm.isBeforeInTranslationUnit(a, b));
-  };
-  auto ordered_comments = std::map<clang::SourceLocation, clang::RawComment*,
-                                   decltype(source_order)>(source_order);
-
-  // We start off by getting the comments from all entry header files...
-  for (const auto& header : invocation_.entry_headers_) {
-    if (auto file = sm.getFileManager().getFile(header.IncludePath())) {
-      if (auto comments = ctx_.Comments.getCommentsInFile(
-              sm.getOrCreateFileID(*file, clang::SrcMgr::C_User))) {
-        for (const auto& [_, comment] : *comments) {
-          ordered_comments.insert({comment->getBeginLoc(), comment});
-        }
-      }
-    }
-  }
-
-  // ... and then we remove those that "conflict" with an IR item.
-  for (const auto& [decl, result] : import_cache_) {
-    if (result.has_value()) {
-      // Remove doc comments of imported items.
-      if (auto raw_comment = ctx_.getRawCommentForDeclNoCache(decl)) {
-        ordered_comments.erase(raw_comment->getBeginLoc());
-      }
-      // Remove comments that are within a visited decl.
-      // TODO(forster): We should retain floating comments in decls like
-      // records and namespaces.
-      ordered_comments.erase(ordered_comments.lower_bound(decl->getBeginLoc()),
-                             ordered_comments.upper_bound(decl->getEndLoc()));
-    }
-  }
-
-  // Return the remaining comments as a `std::vector`.
-  std::vector<clang::RawComment*> result;
-  result.reserve(ordered_comments.size());
-  for (auto& [_, comment] : ordered_comments) {
-    result.push_back(comment);
-  }
-  return result;
-}
-
 // Multiple IR items can be associated with the same source location (e.g. the
 // implicitly defined constructors and assignment operators). To produce
 // deterministic output, we order such items based on GetDeclOrder.  The order
@@ -267,12 +220,30 @@
   return 999;
 }
 
-void Importer::Import(clang::TranslationUnitDecl* translation_unit_decl) {
-  ImportDeclsFromDeclContext(translation_unit_decl);
+class SourceLocationComparator {
+ public:
+  const bool operator()(const clang::SourceLocation& a,
+                        const clang::SourceLocation& b) const {
+    return b.isValid() && a.isValid() && sm.isBeforeInTranslationUnit(a, b);
+  }
+  const bool operator()(const clang::RawComment* a,
+                        const clang::SourceLocation& b) const {
+    return this->operator()(a->getBeginLoc(), b);
+  }
+  const bool operator()(const clang::SourceLocation& a,
+                        const clang::RawComment* b) const {
+    return this->operator()(a, b->getBeginLoc());
+  }
+  const bool operator()(const clang::RawComment* a,
+                        const clang::RawComment* b) const {
+    return this->operator()(a->getBeginLoc(), b->getBeginLoc());
+  }
 
+  using OrderedItemId = std::tuple<clang::SourceRange, int, ItemId>;
   using OrderedItem = std::tuple<clang::SourceRange, int, IR::Item>;
-  clang::SourceManager& sm = ctx_.getSourceManager();
-  auto is_less_than = [&sm](const OrderedItem& a, const OrderedItem& b) {
+
+  template <typename OrderedItemOrId>
+  bool operator()(const OrderedItemOrId& a, const OrderedItemOrId& b) const {
     auto a_range = std::get<0>(a);
     auto b_range = std::get<0>(b);
     if (!a_range.isValid() || !b_range.isValid()) {
@@ -287,82 +258,138 @@
         return sm.isBeforeInTranslationUnit(a_range.getEnd(), b_range.getEnd());
       }
     }
-
     auto a_decl_order = std::get<1>(a);
     auto b_decl_order = std::get<1>(b);
-    if (a_decl_order != b_decl_order) return a_decl_order < b_decl_order;
+    return a_decl_order < b_decl_order;
+  }
 
-    // A single FunctionDecl can be associated with multiple UnsupportedItems.
-    // Comparing the fields allows deterministic order between items like:
-    // Non-trivial_abi type '...' is not supported by value as a parameter.
-    // Non-trivial_abi type '...' is not supported by value as a return type.
-    const auto& a_variant = std::get<2>(a);
-    const auto& b_variant = std::get<2>(b);
-    const auto* a_unsupported = std::get_if<UnsupportedItem>(&a_variant);
-    const auto* b_unsupported = std::get_if<UnsupportedItem>(&b_variant);
-    if (a_unsupported && b_unsupported) {
-      if (a_unsupported->name != b_unsupported->name)
-        return a_unsupported->name < b_unsupported->name;
-      return a_unsupported->message < b_unsupported->message;
+  SourceLocationComparator(clang::SourceManager& sm) : sm(sm) {}
+
+ private:
+  clang::SourceManager& sm;
+};
+
+std::vector<ItemId> Importer::GetItemIdsInSourceOrder(
+    clang::Decl* parent_decl) {
+  auto decl_context = clang::cast<clang::DeclContext>(parent_decl);
+
+  clang::SourceManager& sm = ctx_.getSourceManager();
+  std::vector<SourceLocationComparator::OrderedItemId> items;
+  auto compare_locations = SourceLocationComparator(sm);
+
+  // We are only interested in comments within this decl context.
+  std::vector<const clang::RawComment*> comments_in_range(
+      llvm::lower_bound(comments_, parent_decl->getBeginLoc(),
+                        compare_locations),
+      llvm::upper_bound(comments_, parent_decl->getEndLoc(),
+                        compare_locations));
+
+  std::map<clang::SourceLocation, const clang::RawComment*,
+           SourceLocationComparator>
+      ordered_comments(compare_locations);
+  for (auto& comment : comments_in_range) {
+    ordered_comments.insert({comment->getBeginLoc(), comment});
+  }
+
+  absl::flat_hash_set<ItemId> visited_item_ids;
+  for (auto child : decl_context->decls()) {
+    auto decl = child->getCanonicalDecl();
+    if (!IsFromCurrentTarget(decl)) continue;
+
+    // We remove comments attached to a child decl or that are within a child
+    // decl.
+    if (auto raw_comment = ctx_.getRawCommentForDeclNoCache(decl)) {
+      ordered_comments.erase(raw_comment->getBeginLoc());
     }
+    ordered_comments.erase(ordered_comments.lower_bound(decl->getBeginLoc()),
+                           ordered_comments.upper_bound(decl->getEndLoc()));
 
-    return false;
-  };
-  auto are_equal = [&is_less_than](const OrderedItem& a, const OrderedItem& b) {
-    return !is_less_than(a, b) && !is_less_than(b, a);
-  };
+    // We assume that all the decls of the given parameter decl are already
+    // imported. Some calls to ImportDecl() may have returned std::nullopt, and
+    // those decls don't have corresponding items in the ir, so we don't add
+    // their item id to the list.
+    auto item = import_cache_.find(decl);
+    CRUBIT_CHECK(item != import_cache_.end() && "Found non-imported decl");
+    if (!item->second.has_value()) {
+      continue;
+    }
+    auto item_id = GenerateItemId(decl);
+    // TODO(rosica): Drop this check when we start importing also other redecls,
+    //  not just the canonical
+    if (visited_item_ids.find(item_id) == visited_item_ids.end()) {
+      visited_item_ids.insert(item_id);
+      items.push_back({decl->getSourceRange(), GetDeclOrder(decl), item_id});
+    }
+  }
 
-  // We emit IR items in the order of the decls they were generated for.
-  // For decls that emit multiple items we use a stable, but arbitrary order.
-  std::vector<OrderedItem> items;
+  for (auto& [_, comment] : ordered_comments) {
+    items.push_back({comment->getSourceRange(), 0, GenerateItemId(comment)});
+  }
+  std::sort(items.begin(), items.end(), compare_locations);
+
+  std::vector<ItemId> ordered_item_ids;
+  ordered_item_ids.reserve(items.size());
+  for (auto& ordered_item : items) {
+    ordered_item_ids.push_back(std::get<2>(ordered_item));
+  }
+  return ordered_item_ids;
+}
+
+void Importer::ImportFreeComments() {
+  clang::SourceManager& sm = ctx_.getSourceManager();
+  for (const auto& header : invocation_.entry_headers_) {
+    if (auto file = sm.getFileManager().getFile(header.IncludePath())) {
+      if (auto comments_in_file = ctx_.Comments.getCommentsInFile(
+              sm.getOrCreateFileID(*file, clang::SrcMgr::C_User))) {
+        for (const auto& [_, comment] : *comments_in_file) {
+          comments_.push_back(comment);
+        }
+      }
+    }
+  }
+  std::sort(comments_.begin(), comments_.end(), SourceLocationComparator(sm));
+}
+
+void Importer::Import(clang::TranslationUnitDecl* translation_unit_decl) {
+  ImportFreeComments();
+  clang::SourceManager& sm = ctx_.getSourceManager();
+  std::vector<SourceLocationComparator::OrderedItem> ordered_items;
+
+  for (auto& comment : comments_) {
+    ordered_items.push_back(
+        {comment->getSourceRange(), 0,
+         Comment{.text = comment->getFormattedText(sm, sm.getDiagnostics()),
+                 .id = GenerateItemId(comment)}});
+  }
+
+  ImportDeclsFromDeclContext(translation_unit_decl);
   for (const auto& [decl, item] : import_cache_) {
     if (item.has_value()) {
       if (std::holds_alternative<UnsupportedItem>(*item) &&
           !IsFromCurrentTarget(decl)) {
         continue;
       }
-
-      items.push_back(
-          std::make_tuple(decl->getSourceRange(), GetDeclOrder(decl), *item));
+      ordered_items.push_back(
+          {decl->getSourceRange(), GetDeclOrder(decl), *item});
     }
   }
 
-  for (auto comment : ImportFreeComments()) {
-    items.push_back(std::make_tuple(
-        comment->getSourceRange(), 0 /* decl_order */,
-        Comment{.text = comment->getFormattedText(sm, sm.getDiagnostics()),
-                .id = GenerateItemId(comment)}));
-  }
-  std::sort(items.begin(), items.end(), is_less_than);
+  std::sort(ordered_items.begin(), ordered_items.end(),
+            SourceLocationComparator(sm));
 
-  for (size_t i = 0; i < items.size(); i++) {
-    const auto& item = items[i];
-    if (i > 0) {
-      const auto& prev = items[i - 1];
-      if (are_equal(item, prev)) {
-        llvm::json::Value prev_json = std::visit(
-            [&](auto&& item) { return item.ToJson(); }, std::get<2>(prev));
-        llvm::json::Value curr_json = std::visit(
-            [&](auto&& item) { return item.ToJson(); }, std::get<2>(item));
-        if (prev_json != curr_json) {
-          llvm::report_fatal_error(
-              llvm::formatv("Non-deterministic order of IR items: {0} -VS- {1}",
-                            prev_json, curr_json));
-        } else {
-          // TODO(lukasza): Avoid generating duplicate IR items.  Currently
-          // known example: UnsupportedItem: name=std::signbit; message=
-          // Items contained in namespaces are not supported yet.
-          continue;
-        }
-      }
-    }
-    invocation_.ir_.items.push_back(std::get<2>(item));
+  invocation_.ir_.items.reserve(ordered_items.size());
+  for (auto& ordered_item : ordered_items) {
+    invocation_.ir_.items.push_back(std::get<2>(ordered_item));
   }
+  invocation_.ir_.top_level_item_ids =
+      GetItemIdsInSourceOrder(translation_unit_decl);
 }
 
 void Importer::ImportDeclsFromDeclContext(
     const clang::DeclContext* decl_context) {
   for (auto decl : decl_context->decls()) {
+    // TODO(rosica): We don't always want the canonical decl here (especially
+    // not in namespaces).
     ImportDeclIfNeeded(decl->getCanonicalDecl());
   }
 }
@@ -382,7 +409,8 @@
     return ImportFunction(function_decl);
   } else if (auto* function_template_decl =
                  clang::dyn_cast<clang::FunctionTemplateDecl>(decl)) {
-    return ImportFunction(function_template_decl->getTemplatedDecl());
+    return ImportFunction(function_template_decl->getTemplatedDecl(),
+                          function_template_decl);
   } else if (auto* record_decl = clang::dyn_cast<clang::CXXRecordDecl>(decl)) {
     auto result = ImportRecord(record_decl);
     // TODO(forster): Should we even visit the nested decl if we couldn't
@@ -403,11 +431,12 @@
 }
 
 std::optional<IR::Item> Importer::ImportFunction(
-    clang::FunctionDecl* function_decl) {
+    clang::FunctionDecl* function_decl,
+    clang::FunctionTemplateDecl* function_template_decl) {
   if (!IsFromCurrentTarget(function_decl)) return std::nullopt;
   if (function_decl->isDeleted()) return std::nullopt;
   if (function_decl->isTemplated()) {
-    return ImportUnsupportedItem(function_decl,
+    return ImportUnsupportedItem(function_template_decl,
                                  "Function templates are not supported yet");
   }
 
@@ -604,7 +633,9 @@
         .member_func_metadata = std::move(member_func_metadata),
         .has_c_calling_convention = has_c_calling_convention,
         .source_loc = ConvertSourceLocation(function_decl->getBeginLoc()),
-        .id = GenerateItemId(function_decl),
+        .id = function_template_decl == nullptr
+                  ? GenerateItemId(function_decl)
+                  : GenerateItemId(function_template_decl),
     };
   }
   return std::nullopt;
@@ -716,6 +747,8 @@
     }
   }
 
+  ImportDeclsFromDeclContext(record_decl);
+  auto item_ids = GetItemIdsInSourceOrder(record_decl);
   return Record{
       .rs_name = std::string(record_name->Ident()),
       .cc_name = std::string(record_name->Ident()),
@@ -734,7 +767,8 @@
       .is_trivial_abi = record_decl->canPassInRegisters(),
       .is_inheritable =
           !record_decl->isEffectivelyFinal() && !record_decl->isUnion(),
-      .is_union = record_decl->isUnion()};
+      .is_union = record_decl->isUnion(),
+      .child_item_ids = std::move(item_ids)};
 }
 
 std::optional<IR::Item> Importer::ImportEnum(clang::EnumDecl* enum_decl) {
diff --git a/rs_bindings_from_cc/importer.h b/rs_bindings_from_cc/importer.h
index f989e6e..d20197f 100644
--- a/rs_bindings_from_cc/importer.h
+++ b/rs_bindings_from_cc/importer.h
@@ -106,7 +106,9 @@
 
   // These functions import specific `Decl` subtypes. They use `LookupDecl` to
   // lookup dependencies. They don't use or update the cache themselves.
-  std::optional<IR::Item> ImportFunction(clang::FunctionDecl* function_decl);
+  std::optional<IR::Item> ImportFunction(
+      clang::FunctionDecl* function_decl,
+      clang::FunctionTemplateDecl* function_template_decl = nullptr);
   std::optional<IR::Item> ImportRecord(clang::CXXRecordDecl* record_decl);
   std::optional<IR::Item> ImportTypedefName(
       clang::TypedefNameDecl* typedef_name_decl);
@@ -118,7 +120,12 @@
 
   absl::StatusOr<std::vector<Field>> ImportFields(
       clang::CXXRecordDecl* record_decl);
-  std::vector<clang::RawComment*> ImportFreeComments();
+  // Stores the comments of this target in source order.
+  void ImportFreeComments();
+  // Returns the item ids of the children and comments of the given decl in
+  // source order. This method assumes that the children decls have already been
+  // imported.
+  std::vector<ItemId> GetItemIdsInSourceOrder(clang::Decl* decl);
 
   std::string GetMangledName(const clang::NamedDecl* named_decl) const;
   BazelLabel GetOwningTarget(const clang::Decl* decl) const;
@@ -185,6 +192,7 @@
   absl::flat_hash_map<const clang::Decl*, std::optional<IR::Item>>
       import_cache_;
   absl::flat_hash_set<const clang::TypeDecl*> known_type_decls_;
+  std::vector<const clang::RawComment*> comments_;
 };  // class Importer
 
 }  // namespace crubit
diff --git a/rs_bindings_from_cc/importer_test.cc b/rs_bindings_from_cc/importer_test.cc
index 9541618..808f513 100644
--- a/rs_bindings_from_cc/importer_test.cc
+++ b/rs_bindings_from_cc/importer_test.cc
@@ -24,12 +24,14 @@
 
 using ::testing::AllOf;
 using ::testing::AnyOf;
+using ::testing::Contains;
 using ::testing::Each;
 using ::testing::ElementsAre;
 using ::testing::IsEmpty;
 using ::testing::Not;
 using ::testing::Pointee;
 using ::testing::SizeIs;
+using ::testing::UnorderedElementsAre;
 using ::testing::VariantWith;
 using ::testing::status::StatusIs;
 
@@ -42,6 +44,23 @@
   return std::nullopt;
 }
 
+std::optional<IR::Item> FindItemById(const IR& ir, ItemId id) {
+  for (auto item : ir.items) {
+    if (auto* record = std::get_if<Record>(&item); record && record->id == id) {
+      return item;
+    } else if (auto* func = std::get_if<Func>(&item); func && func->id == id) {
+      return item;
+    } else if (auto* comment = std::get_if<Comment>(&item);
+               comment && comment->id == id) {
+      return item;
+    } else if (auto* unsupported = std::get_if<UnsupportedItem>(&item);
+               unsupported && unsupported->id == id) {
+      return item;
+    }
+  }
+  return std::nullopt;
+}
+
 template <typename T>
 UnqualifiedIdentifier GetName(const T& x) {
   return x.identifier;
@@ -111,6 +130,14 @@
   return false;
 }
 
+// Matches text for comments.
+MATCHER_P(TextIs, text, "") {
+  if (arg.text == text) return true;
+
+  *result_listener << "actual text: '" << arg.text << "'";
+  return false;
+}
+
 // Matches an RsType or CcType that has the given decl_id.
 MATCHER_P(DeclIdIs, decl_id, "") {
   if (arg.decl_id.hasValue() && *arg.decl_id == decl_id) return true;
@@ -301,7 +328,7 @@
 TEST(ImporterTest, FuncWithVoidReturnType) {
   ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc("void Foo();"));
   EXPECT_THAT(ItemsWithoutBuiltins(ir),
-              ElementsAre(VariantWith<Func>(
+              UnorderedElementsAre(VariantWith<Func>(
                   AllOf(IdentifierIs("Foo"), MangledNameIs("_Z3Foov"),
                         ReturnType(IsVoid()), ParamsAre()))));
 }
@@ -310,7 +337,7 @@
   ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc("void Foo(); void Bar();"));
   EXPECT_THAT(
       ItemsWithoutBuiltins(ir),
-      ElementsAre(
+      UnorderedElementsAre(
           VariantWith<Func>(AllOf(IdentifierIs("Foo"), MangledNameIs("_Z3Foov"),
                                   ReturnType(IsVoid()), ParamsAre())),
           VariantWith<Func>(AllOf(IdentifierIs("Bar"), MangledNameIs("_Z3Barv"),
@@ -331,35 +358,36 @@
                            BazelLabel{"//two_funcs:one_target"}},
                       }));
   EXPECT_THAT(ItemsWithoutBuiltins(ir),
-              ElementsAre(VariantWith<Func>(IdentifierIs("Foo")),
-                          VariantWith<Func>(IdentifierIs("Bar"))));
+              UnorderedElementsAre(VariantWith<Func>(IdentifierIs("Foo")),
+                                   VariantWith<Func>(IdentifierIs("Bar"))));
 }
 
 TEST(ImporterTest, NonInlineFunc) {
   ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc("void Foo() {}"));
   EXPECT_THAT(ItemsWithoutBuiltins(ir),
-              ElementsAre(VariantWith<Func>(
+              UnorderedElementsAre(VariantWith<Func>(
                   AllOf(IdentifierIs("Foo"), Not(IsInline())))));
 }
 
 TEST(ImporterTest, InlineFunc) {
   ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc("inline void Foo() {}"));
-  EXPECT_THAT(
-      ItemsWithoutBuiltins(ir),
-      ElementsAre(VariantWith<Func>(AllOf(IdentifierIs("Foo"), IsInline()))));
+  EXPECT_THAT(ItemsWithoutBuiltins(ir),
+              UnorderedElementsAre(
+                  VariantWith<Func>(AllOf(IdentifierIs("Foo"), IsInline()))));
 }
 
 TEST(ImporterTest, FuncJustOnce) {
   ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc("void Foo(); void Foo();"));
-  EXPECT_THAT(ItemsWithoutBuiltins(ir),
-              ElementsAre(VariantWith<Func>(AllOf(IdentifierIs("Foo")))));
+  EXPECT_THAT(
+      ItemsWithoutBuiltins(ir),
+      UnorderedElementsAre(VariantWith<Func>(AllOf(IdentifierIs("Foo")))));
 }
 
 TEST(ImporterTest, TestImportPointerFunc) {
   ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc("int* Foo(int* a);"));
 
   EXPECT_THAT(ItemsWithoutBuiltins(ir),
-              ElementsAre(VariantWith<Func>(AllOf(
+              UnorderedElementsAre(VariantWith<Func>(AllOf(
                   ReturnType(IsIntPtr()), ParamsAre(ParamType(IsIntPtr()))))));
 }
 
@@ -383,7 +411,7 @@
   ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc("int& Foo(int& a);"));
 
   EXPECT_THAT(ItemsWithoutBuiltins(ir),
-              ElementsAre(VariantWith<Func>(AllOf(
+              UnorderedElementsAre(VariantWith<Func>(AllOf(
                   ReturnType(IsIntRef()), ParamsAre(ParamType(IsIntRef()))))));
 }
 
@@ -776,5 +804,73 @@
   EXPECT_THAT(records, Each(Pointee(Not(IsTrivialAbi()))));
 }
 
+TEST(ImporterTest, TopLevelItemIds) {
+  absl::string_view file = R"cc(
+    struct TopLevelStruct {};
+    // Top level comment
+
+    // Function comment
+    void top_level_func();
+    namespace top_level_namespace {
+    struct Nested {};
+    // free nested comment
+
+    // nested_func comment
+    void nested_func();
+    }  // namespace top_level_namespace
+  )cc";
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc(file));
+
+  std::vector<IR::Item> items;
+  for (const auto& id : ir.top_level_item_ids) {
+    auto item = FindItemById(ir, id);
+    ASSERT_TRUE(item.has_value());
+    items.push_back(*item);
+  }
+
+  EXPECT_THAT(ir.top_level_item_ids, SizeIs(5));
+  EXPECT_THAT(
+      items,
+      ElementsAre(
+          VariantWith<Record>(RsNameIs("TopLevelStruct")),
+          VariantWith<Comment>(TextIs("Top level comment")),
+          VariantWith<Func>(IdentifierIs("top_level_func")),
+          VariantWith<UnsupportedItem>(NameIs("top_level_namespace")),
+          VariantWith<Comment>(TextIs("namespace top_level_namespace"))));
+}
+
+TEST(ImporterTest, RecordItemIds) {
+  absl::string_view file = R"cc(
+    struct TopLevelStruct {
+      // A free comment
+
+      // foo comment
+      int foo;
+
+      int bar();
+      struct Nested {};
+      int baz();
+    };
+  )cc";
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc(file));
+
+  std::vector<const Record*> records = ir.get_items_if<Record>();
+  EXPECT_THAT(records, SizeIs(1));
+
+  std::vector<IR::Item> items;
+  for (const auto& id : records[0]->child_item_ids) {
+    auto item = FindItemById(ir, id);
+    ASSERT_TRUE(item.has_value());
+    items.push_back(*item);
+  }
+
+  EXPECT_THAT(items,
+              AllOf(Contains(VariantWith<Comment>(TextIs("A free comment"))),
+                    Contains(VariantWith<Func>(IdentifierIs("bar"))),
+                    Contains(VariantWith<UnsupportedItem>(
+                        NameIs("TopLevelStruct::Nested"))),
+                    Contains(VariantWith<Func>(IdentifierIs("baz")))));
+}
+
 }  // namespace
 }  // namespace crubit
diff --git a/rs_bindings_from_cc/ir.cc b/rs_bindings_from_cc/ir.cc
index 3d9d8a5..675d122 100644
--- a/rs_bindings_from_cc/ir.cc
+++ b/rs_bindings_from_cc/ir.cc
@@ -360,6 +360,12 @@
 }
 
 llvm::json::Value Record::ToJson() const {
+  std::vector<llvm::json::Value> json_item_ids;
+  json_item_ids.reserve(child_item_ids.size());
+  for (const auto& id : child_item_ids) {
+    json_item_ids.push_back(id.value());
+  }
+
   llvm::json::Object record{
       {"rs_name", rs_name},
       {"cc_name", cc_name},
@@ -379,6 +385,7 @@
       {"is_trivial_abi", is_trivial_abi},
       {"is_inheritable", is_inheritable},
       {"is_union", is_union},
+      {"child_item_ids", std::move(json_item_ids)},
   };
 
   return llvm::json::Object{
@@ -443,7 +450,7 @@
       {"text", text},
       {"id", id},
   };
-
+  comment["id"] = id.value();
   return llvm::json::Object{
       {"Comment", std::move(comment)},
   };
@@ -456,10 +463,17 @@
     std::visit([&](auto&& item) { json_items.push_back(item.ToJson()); }, item);
   }
 
+  std::vector<llvm::json::Value> top_level_ids;
+  top_level_ids.reserve(top_level_item_ids.size());
+  for (const auto& id : top_level_item_ids) {
+    top_level_ids.push_back(id.value());
+  }
+
   return llvm::json::Object{
       {"used_headers", used_headers},
       {"current_target", current_target},
       {"items", std::move(json_items)},
+      {"top_level_item_ids", std::move(top_level_ids)},
   };
 }
 
diff --git a/rs_bindings_from_cc/ir.h b/rs_bindings_from_cc/ir.h
index aeae4d1..2936899 100644
--- a/rs_bindings_from_cc/ir.h
+++ b/rs_bindings_from_cc/ir.h
@@ -571,6 +571,8 @@
 
   // Whether this type is a C++ union (rather than a struct)
   bool is_union = false;
+
+  std::vector<ItemId> child_item_ids;
 };
 
 struct Enumerator {
@@ -665,6 +667,7 @@
   using Item =
       std::variant<Func, Record, Enum, TypeAlias, UnsupportedItem, Comment>;
   std::vector<Item> items;
+  std::vector<ItemId> top_level_item_ids;
 };
 
 inline std::ostream& operator<<(std::ostream& o, const IR& ir) {
diff --git a/rs_bindings_from_cc/ir.rs b/rs_bindings_from_cc/ir.rs
index 2c9c684..0ccdebc 100644
--- a/rs_bindings_from_cc/ir.rs
+++ b/rs_bindings_from_cc/ir.rs
@@ -29,6 +29,7 @@
         items.into_iter().collect_vec(),
         /* used_headers= */ vec![],
         /* current_target= */ TESTING_TARGET.into(),
+        /* top_level_item_ids= */ vec![],
     )
 }
 
@@ -38,8 +39,9 @@
     items: Vec<Item>,
     used_headers: Vec<HeaderName>,
     current_target: BazelLabel,
+    top_level_item_ids: Vec<ItemId>,
 ) -> Result<IR> {
-    make_ir(FlatIR { used_headers, current_target, items })
+    make_ir(FlatIR { used_headers, current_target, items, top_level_item_ids })
 }
 
 fn make_ir(flat_ir: FlatIR) -> Result<IR> {
@@ -365,6 +367,7 @@
     pub is_trivial_abi: bool,
     pub is_inheritable: bool,
     pub is_union: bool,
+    pub child_item_ids: Vec<ItemId>,
 }
 
 impl Record {
@@ -542,6 +545,8 @@
     current_target: BazelLabel,
     #[serde(default)]
     items: Vec<Item>,
+    #[serde(default)]
+    top_level_item_ids: Vec<ItemId>,
 }
 
 /// Struct providing the necessary information about the API of a C++ target to
@@ -560,6 +565,10 @@
         self.flat_ir.items.iter()
     }
 
+    pub fn top_level_item_ids(&self) -> impl Iterator<Item = &ItemId> {
+        self.flat_ir.top_level_item_ids.iter()
+    }
+
     pub fn items_mut(&mut self) -> impl Iterator<Item = &mut Item> {
         self.flat_ir.items.iter_mut()
     }
@@ -707,6 +716,7 @@
         let expected = FlatIR {
             used_headers: vec![HeaderName { name: "foo/bar.h".to_string() }],
             current_target: "//foo:bar".into(),
+            top_level_item_ids: vec![],
             items: vec![],
         };
         assert_eq!(ir.flat_ir, expected);
diff --git a/rs_bindings_from_cc/ir_from_cc_test.rs b/rs_bindings_from_cc/ir_from_cc_test.rs
index 13a1daf..96d0443 100644
--- a/rs_bindings_from_cc/ir_from_cc_test.rs
+++ b/rs_bindings_from_cc/ir_from_cc_test.rs
@@ -10,7 +10,7 @@
 use quote::quote;
 use std::collections::{HashMap, HashSet};
 use std::iter::Iterator;
-use token_stream_matchers::{assert_ir_matches, assert_ir_not_matches};
+use token_stream_matchers::{assert_ir_matches, assert_ir_not_matches, assert_items_match};
 
 #[test]
 fn test_function() {
@@ -831,6 +831,7 @@
                base_size: Some(4),
                override_alignment: true,
                ...
+               ...
            }),
         }
     );
@@ -1463,3 +1464,117 @@
         ir.functions().find(|i| i.name == UnqualifiedIdentifier::Identifier(ir_id("foo"))).unwrap();
     assert_ne!(function.id, ItemId(0));
 }
+
+#[test]
+fn test_top_level_items() {
+    let ir = ir_from_cc(
+        r#"
+        struct TopLevelStruct {};
+        // Top level comment
+
+        // Function comment
+        void top_level_func();
+        namespace top_level_namespace {
+        struct Nested {};
+        // free nested comment
+
+        // nested_func comment
+        void nested_func();
+        }  // namespace top_level_namespace"#,
+    )
+    .unwrap();
+
+    let top_level_items =
+        ir.top_level_item_ids().map(|id| ir.find_decl(*id).unwrap()).collect_vec();
+
+    assert_items_match!(
+        top_level_items,
+        vec![
+            quote! {
+              Record {
+                ... rs_name: "TopLevelStruct" ...
+              }
+            },
+            quote! {
+              Comment {
+                ... text: "Top level comment" ...
+              }
+            },
+            quote! {
+              Func { ... name: "top_level_func" ... }
+            },
+            quote! {
+              UnsupportedItem { ... name: "top_level_namespace" ... }
+            },
+            quote! {
+              Comment {
+                ... text: "namespace top_level_namespace" ...
+              }
+            },
+        ]
+    );
+}
+
+#[test]
+fn test_record_items() {
+    let ir = ir_from_cc(
+        r#"
+        struct TopLevelStruct {
+          // A free comment
+
+          // foo comment
+          int foo;
+
+          int bar();
+          struct Nested {};
+          int baz();
+        };"#,
+    )
+    .unwrap();
+
+    let record = ir.records().find(|i| i.rs_name.as_str() == "TopLevelStruct").unwrap();
+    let record_items =
+        record.child_item_ids.iter().map(|id| ir.find_decl(*id).unwrap()).collect_vec();
+
+    assert_items_match!(
+        record_items,
+        vec![
+            quote! {
+              Func { ... name: Constructor ... }
+            },
+            quote! {
+              Func { ... name: Constructor ... }
+            },
+            quote! {
+              // Unsupported parameter
+              UnsupportedItem { ... name: "TopLevelStruct::TopLevelStruct", ... }
+            },
+            quote! {
+              Func { ... name: Destructor ... }
+            },
+            quote! {
+              Func { ... name: "operator=" ... }
+            },
+            quote! {
+              // Unsupported parameter
+              UnsupportedItem { ... name: "TopLevelStruct::operator=" ... }
+            },
+            quote! {
+              ...Comment {
+                ... text: "A free comment" ...
+              }
+            },
+            quote! {
+              ... Func { ... name: "bar" ... }
+            },
+            quote! {
+              ... UnsupportedItem { ... name: "TopLevelStruct::Nested" ... }
+            },
+            quote! {
+              ...Func {
+                ... name: "baz" ...
+              }
+            },
+        ]
+    );
+}
diff --git a/rs_bindings_from_cc/src_code_gen.rs b/rs_bindings_from_cc/src_code_gen.rs
index 44b46cb..0060716 100644
--- a/rs_bindings_from_cc/src_code_gen.rs
+++ b/rs_bindings_from_cc/src_code_gen.rs
@@ -837,7 +837,11 @@
 
 /// Generates Rust source code for a given `Record` and associated assertions as
 /// a tuple.
-fn generate_record(record: &Record, ir: &IR) -> Result<(RsSnippet, RsSnippet)> {
+fn generate_record(
+    record: &Record,
+    ir: &IR,
+    overloaded_funcs: &HashSet<FunctionId>,
+) -> Result<GeneratedItem> {
     let ident = make_rs_ident(&record.rs_name);
     let doc_comment = generate_doc_comment(&record.doc_comment);
     let field_idents =
@@ -975,6 +979,30 @@
     let no_unique_address_accessors = cc_struct_no_unique_address_impl(record, ir)?;
     let base_class_into = cc_struct_upcast_impl(record, ir)?;
 
+    let record_generated_items = record
+        .child_item_ids
+        .iter()
+        .map(|id| {
+            let item = ir.find_decl(*id)?;
+            generate_item(item, ir, overloaded_funcs)
+        })
+        .collect::<Result<Vec<_>>>()?;
+
+    let mut items = vec![];
+    let mut thunks_from_record_items = vec![];
+    let mut assertions_from_record_items = vec![];
+
+    for generated in record_generated_items.iter() {
+        items.push(&generated.item);
+        if !generated.thunks.is_empty() {
+            thunks_from_record_items.push(&generated.thunks);
+        }
+        if !generated.assertions.is_empty() {
+            assertions_from_record_items.push(&generated.assertions);
+        }
+        record_features.extend(generated.features.clone());
+    }
+
     let record_tokens = quote! {
         #doc_comment
         #derives
@@ -990,6 +1018,9 @@
         #base_class_into
 
         #unpin_impl
+
+        __NEWLINE__ __NEWLINE__
+        #( #items __NEWLINE__ __NEWLINE__)*
     };
 
     let record_trait_assertions = {
@@ -1024,12 +1055,20 @@
         #( #record_trait_assertions )*
         #( #field_offset_assertions )*
         #( #field_copy_trait_assertions )*
+        #( #assertions_from_record_items )*
     };
 
-    Ok((
-        RsSnippet { features: record_features, tokens: record_tokens },
-        RsSnippet { features: assertion_features, tokens: assertion_tokens },
-    ))
+    let thunk_tokens = quote! {
+        #( #thunks_from_record_items )*
+    };
+
+    Ok(GeneratedItem {
+        item: record_tokens,
+        features: record_features.union(&assertion_features).cloned().collect(),
+        assertions: assertion_tokens,
+        thunks: thunk_tokens,
+        has_record: true,
+    })
 }
 
 fn should_derive_clone(record: &Record) -> bool {
@@ -1162,18 +1201,7 @@
             {
                 GeneratedItem { ..Default::default() }
             } else {
-                let (snippet, assertions_snippet) = generate_record(record, ir)?;
-                GeneratedItem {
-                    item: snippet.tokens,
-                    assertions: assertions_snippet.tokens,
-                    features: snippet
-                        .features
-                        .union(&assertions_snippet.features)
-                        .cloned()
-                        .collect(),
-                    has_record: true,
-                    ..Default::default()
-                }
+                generate_record(record, ir, overloaded_funcs)?
             }
         }
         Item::Enum(enum_) => {
@@ -1239,7 +1267,8 @@
         }
     }
 
-    for item in ir.items() {
+    for top_level_item_id in ir.top_level_item_ids() {
+        let item = ir.find_decl(*top_level_item_id)?;
         let generated = generate_item(item, ir, &overloaded_funcs)?;
         items.push(generated.item);
         if !generated.thunks.is_empty() {
diff --git a/rs_bindings_from_cc/test/golden/clang_attrs_rs_api.rs b/rs_bindings_from_cc/test/golden/clang_attrs_rs_api.rs
index 5b9fc16..4745bc5 100644
--- a/rs_bindings_from_cc/test/golden/clang_attrs_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/clang_attrs_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/comment.h b/rs_bindings_from_cc/test/golden/comment.h
index 241af47..d290c0c 100644
--- a/rs_bindings_from_cc/test/golden/comment.h
+++ b/rs_bindings_from_cc/test/golden/comment.h
@@ -15,17 +15,20 @@
 
 /// Foo
 struct Foo final {
-  // Foo a
+  // TODO(rosica): This comment appears near fields of a struct, and
+  // is currently generated below the struct definiton on the Rust side.
 
   /// A field
   int i;
 
-  // Foo b
+  // TODO(rosica): This comment appears between fields of a struct, and
+  // is currently generated below the struct definiton on the Rust side.
 
   /// Another field
   int j;
 
-  // Foo c
+  // TODO(rosica): This comment appears near fields of a struct, and
+  // is currently generated below the struct definiton on the Rust side.
 };
 
 // b
diff --git a/rs_bindings_from_cc/test/golden/comment_rs_api.rs b/rs_bindings_from_cc/test/golden/comment_rs_api.rs
index 3ce9927..f6a94e1 100644
--- a/rs_bindings_from_cc/test/golden/comment_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/comment_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
@@ -65,6 +63,15 @@
 // Error while generating bindings for item 'Foo::operator=':
 // Bindings for this kind of operator are not supported
 
+// TODO(rosica): This comment appears near fields of a struct, and
+// is currently generated below the struct definiton on the Rust side.
+
+// TODO(rosica): This comment appears between fields of a struct, and
+// is currently generated below the struct definiton on the Rust side.
+
+// TODO(rosica): This comment appears near fields of a struct, and
+// is currently generated below the struct definiton on the Rust side.
+
 // b
 
 // }  // namespace ns
@@ -106,11 +113,11 @@
     }
 }
 
-// rs_bindings_from_cc/test/golden/comment.h;l=43
+// rs_bindings_from_cc/test/golden/comment.h;l=46
 // Error while generating bindings for item 'Bar::operator=':
 // Bindings for this kind of operator are not supported
 
-// rs_bindings_from_cc/test/golden/comment.h;l=43
+// rs_bindings_from_cc/test/golden/comment.h;l=46
 // Error while generating bindings for item 'Bar::operator=':
 // Bindings for this kind of operator are not supported
 
@@ -143,11 +150,11 @@
     }
 }
 
-// rs_bindings_from_cc/test/golden/comment.h;l=49
+// rs_bindings_from_cc/test/golden/comment.h;l=52
 // Error while generating bindings for item 'HasNoComments::operator=':
 // Bindings for this kind of operator are not supported
 
-// rs_bindings_from_cc/test/golden/comment.h;l=49
+// rs_bindings_from_cc/test/golden/comment.h;l=52
 // Error while generating bindings for item 'HasNoComments::operator=':
 // Bindings for this kind of operator are not supported
 
diff --git a/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs b/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
index 3de6bc3..8506741 100644
--- a/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/escaping_keywords_rs_api.rs b/rs_bindings_from_cc/test/golden/escaping_keywords_rs_api.rs
index be9558b..ced4f4a 100644
--- a/rs_bindings_from_cc/test/golden/escaping_keywords_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/escaping_keywords_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
@@ -64,7 +62,7 @@
 // Error while generating bindings for item 'await':
 // Class templates are not supported yet
 
-// rs_bindings_from_cc/test/golden/escaping_keywords.h;l=22
+// rs_bindings_from_cc/test/golden/escaping_keywords.h;l=21
 // Error while generating bindings for item 'become':
 // Function templates are not supported yet
 
diff --git a/rs_bindings_from_cc/test/golden/inheritance_rs_api.rs b/rs_bindings_from_cc/test/golden/inheritance_rs_api.rs
index 8f5d692..3522e20 100644
--- a/rs_bindings_from_cc/test/golden/inheritance_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/inheritance_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/item_order_rs_api.rs b/rs_bindings_from_cc/test/golden/item_order_rs_api.rs
index c5101af..445e018 100644
--- a/rs_bindings_from_cc/test/golden/item_order_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/item_order_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/lifetimes_rs_api.rs b/rs_bindings_from_cc/test/golden/lifetimes_rs_api.rs
index 3d82054..9715b34 100644
--- a/rs_bindings_from_cc/test/golden/lifetimes_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/lifetimes_rs_api.rs
@@ -11,8 +11,6 @@
 
 use ::std as rust_std;
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/no_elided_lifetimes_rs_api.rs b/rs_bindings_from_cc/test/golden/no_elided_lifetimes_rs_api.rs
index 6709bdb..2d60078 100644
--- a/rs_bindings_from_cc/test/golden/no_elided_lifetimes_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/no_elided_lifetimes_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/nontrivial_type_rs_api.rs b/rs_bindings_from_cc/test/golden/nontrivial_type_rs_api.rs
index f5a4fe4..4de66ae 100644
--- a/rs_bindings_from_cc/test/golden/nontrivial_type_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/nontrivial_type_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/polymorphic_rs_api.rs b/rs_bindings_from_cc/test/golden/polymorphic_rs_api.rs
index 848089c..daf2907 100644
--- a/rs_bindings_from_cc/test/golden/polymorphic_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/polymorphic_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/private_members_rs_api.rs b/rs_bindings_from_cc/test/golden/private_members_rs_api.rs
index e71d8c8..c6b3703 100644
--- a/rs_bindings_from_cc/test/golden/private_members_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/private_members_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/static_methods_rs_api.rs b/rs_bindings_from_cc/test/golden/static_methods_rs_api.rs
index 11c82eb..32f251b 100644
--- a/rs_bindings_from_cc/test/golden/static_methods_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/static_methods_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/trivial_type_rs_api.rs b/rs_bindings_from_cc/test/golden/trivial_type_rs_api.rs
index 968c268..50212c5 100644
--- a/rs_bindings_from_cc/test/golden/trivial_type_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/trivial_type_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/types_rs_api.rs b/rs_bindings_from_cc/test/golden/types_rs_api.rs
index 8182540..d678482 100644
--- a/rs_bindings_from_cc/test/golden/types_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/types_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
@@ -160,6 +158,10 @@
     }
 }
 
+// TODO(b/226580208): Uncomment when these don't cause struct import to fail.
+// SomeStruct&& struct_rvalue_ref_field;
+// const SomeStruct&& const_struct_rvalue_ref_field;
+
 #[derive(Clone, Copy)]
 #[repr(C)]
 pub union NonEmptyUnion {
diff --git a/rs_bindings_from_cc/test/golden/unsupported_rs_api.rs b/rs_bindings_from_cc/test/golden/unsupported_rs_api.rs
index 449c754..132d849 100644
--- a/rs_bindings_from_cc/test/golden/unsupported_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/unsupported_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
@@ -104,14 +102,6 @@
 // Error while generating bindings for item 'ContainingStruct::NestedStruct':
 // Nested classes are not supported yet
 
-// rs_bindings_from_cc/test/golden/unsupported.h;l=32
-// Error while generating bindings for item 'ContainingStruct::NestedStruct::NonStaticMemberFunction':
-// Couldn't import the parent
-
-// rs_bindings_from_cc/test/golden/unsupported.h;l=33
-// Error while generating bindings for item 'ContainingStruct::NestedStruct::StaticMemberFunction':
-// Couldn't import the parent
-
 // CRUBIT_RS_BINDINGS_FROM_CC_TEST_GOLDEN_UNSUPPORTED_H_
 
 mod detail {
diff --git a/rs_bindings_from_cc/test/golden/user_of_base_class_rs_api.rs b/rs_bindings_from_cc/test/golden/user_of_base_class_rs_api.rs
index 1a01da6..af9f2ad 100644
--- a/rs_bindings_from_cc/test/golden/user_of_base_class_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/user_of_base_class_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/user_of_imported_type_rs_api.rs b/rs_bindings_from_cc/test/golden/user_of_imported_type_rs_api.rs
index 263beda..0d7d3dc 100644
--- a/rs_bindings_from_cc/test/golden/user_of_imported_type_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/user_of_imported_type_rs_api.rs
@@ -13,8 +13,6 @@
 use memoffset_unstable_const::offset_of;
 use static_assertions::{assert_impl_all, assert_not_impl_all};
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/test/golden/user_of_unsupported_rs_api.rs b/rs_bindings_from_cc/test/golden/user_of_unsupported_rs_api.rs
index 0ebd231..1ca30dd 100644
--- a/rs_bindings_from_cc/test/golden/user_of_unsupported_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/user_of_unsupported_rs_api.rs
@@ -11,8 +11,6 @@
 
 use ::std as rust_std;
 
-pub type __builtin_ms_va_list = *mut u8;
-
 // Part of the Crubit project, under the Apache License v2.0 with LLVM
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
diff --git a/rs_bindings_from_cc/token_stream_matchers.rs b/rs_bindings_from_cc/token_stream_matchers.rs
index 62e1333..bdcb6c0 100644
--- a/rs_bindings_from_cc/token_stream_matchers.rs
+++ b/rs_bindings_from_cc/token_stream_matchers.rs
@@ -104,11 +104,30 @@
     };
 }
 
+/// Like `assert_ir_matches!`, but expects a list of Items and a list of
+/// TokenStreams as input. The macro converts each Item to its struct expression
+/// and matches the corresponding pattern against that.
+/// The use case for this macro is to compare a list of Items to expected
+/// patterns, e.g when we want to confirm that the children items of an item
+/// appear in a certain order.
+#[macro_export]
+macro_rules! assert_items_match {
+    ($items:expr, $patterns:expr $(,)*) => {
+        assert_eq!($items.len(), $patterns.len());
+        for (idx, (item, pattern)) in itertools::enumerate(itertools::zip($items, $patterns)) {
+            $crate::internal::match_item(&item, &pattern).expect(&format!(
+                "input at position {} unexpectedly didn't match the pattern",
+                &idx
+            ));
+        }
+    };
+}
+
 /// Only used to make stuff needed by exported macros available
 pub mod internal {
 
     use anyhow::{anyhow, Result};
-    use ir::IR;
+    use ir::{Item, IR};
     use itertools::Itertools;
     pub use proc_macro2::TokenStream;
     use proc_macro2::TokenTree;
@@ -150,6 +169,19 @@
         Ok(snippet)
     }
 
+    pub fn match_item(item: &Item, pattern: &TokenStream) -> Result<()> {
+        match_tokens(&item_to_token_stream(item)?, pattern, &ir_to_string)
+    }
+
+    fn item_to_token_stream(item: &Item) -> Result<TokenStream> {
+        // derived debug impl doesn't emit commas after the last element of a group,
+        // (for example `[a, b, c]`), but rustfmt automatically adds it (`[a, b,
+        // c,]`). We use rustfmt to format the failure messages. So to make the
+        // input token stream consistent with failure messages we format the
+        // input token stream with rustfmt as well.
+        Ok(ir_to_string(format! {"{:?}", item}.parse().unwrap())?.parse().unwrap())
+    }
+
     #[derive(Debug)]
     enum MatchInfo {
         // Successful match with the suffix of the `input` stream that follows the match.
@@ -470,7 +502,10 @@
 
     #[test]
     fn test_assert_ir_matches_assumes_trailing_commas_in_groups() {
-        assert_ir_matches!(ir_from_cc("").unwrap(), quote! {{... items: [...],}});
+        assert_ir_matches!(
+            ir_from_cc("").unwrap(),
+            quote! {{... items: [...], top_level_item_ids: [...], }}
+        );
     }
 
     #[test]