Просмотр исходного кода

Add support for `inline Cpp` declarations. (#6994)

For #6830, add support for inline C++ fragments as a declaration rather
than as a packaging directive. For now, this uses `inline Cpp
<string-literal>;` as syntax. The prior `import Cpp inline
<string-literal>;` is left alone for the time being. We can decide
separately whether to remove that.

`inline Cpp` requires that there was at least one `import Cpp`. It's not
clear to me if that's the right design long-term, but it seems
reasonable for now.

Assisted-by: Gemini via Google Antigravity
Richard Smith 1 месяц назад
Родитель
Сommit
8b59e85b16

+ 88 - 32
toolchain/check/cpp/generate_ast.cpp

@@ -49,31 +49,37 @@ static auto GenerateLineMarker(Context& context, llvm::raw_ostream& out,
       << FormatEscaped(context.tokens().source().filename()) << "\"\n";
       << FormatEscaped(context.tokens().source().filename()) << "\"\n";
 }
 }
 
 
+// Appends a line marker and the specified `code` to `out`, adjusting the
+// `line` number if the `code_token` represents a block string literal.
+static auto AppendInlineCode(Context& context, llvm::raw_ostream& out,
+                             Lex::TokenIndex code_token, llvm::StringRef code)
+    -> void {
+  // Compute the line number on which the C++ code starts. Usually the code
+  // is specified as a block string literal and starts on the line after the
+  // start of the string token.
+  // TODO: Determine if this is a block string literal without calling
+  // `GetTokenText`, which re-lexes the string.
+  int line = context.tokens().GetLineNumber(code_token);
+  if (context.tokens().GetTokenText(code_token).contains('\n')) {
+    ++line;
+  }
+
+  GenerateLineMarker(context, out, line);
+  out << code << "\n";
+}
+
 // Generates C++ file contents to #include all requested imports.
 // Generates C++ file contents to #include all requested imports.
 static auto GenerateCppIncludesHeaderCode(
 static auto GenerateCppIncludesHeaderCode(
     Context& context, llvm::ArrayRef<Parse::Tree::PackagingNames> imports)
     Context& context, llvm::ArrayRef<Parse::Tree::PackagingNames> imports)
     -> std::string {
     -> std::string {
-  std::string code;
-  llvm::raw_string_ostream code_stream(code);
+  RawStringOstream code_stream;
   for (const Parse::Tree::PackagingNames& import : imports) {
   for (const Parse::Tree::PackagingNames& import : imports) {
     if (import.inline_body_id.has_value()) {
     if (import.inline_body_id.has_value()) {
       // Expand `import Cpp inline "code";` directly into the specified code.
       // Expand `import Cpp inline "code";` directly into the specified code.
       auto code_token = context.parse_tree().node_token(import.inline_body_id);
       auto code_token = context.parse_tree().node_token(import.inline_body_id);
-
-      // Compute the line number on which the C++ code starts. Usually the code
-      // is specified as a block string literal and starts on the line after the
-      // start of the string token.
-      // TODO: Determine if this is a block string literal without calling
-      // `GetTokenText`, which re-lexes the string.
-      int line = context.tokens().GetLineNumber(code_token);
-      if (context.tokens().GetTokenText(code_token).contains('\n')) {
-        ++line;
-      }
-
-      GenerateLineMarker(context, code_stream, line);
-      code_stream << context.string_literal_values().Get(
-                         context.tokens().GetStringLiteralValue(code_token))
-                  << "\n";
+      AppendInlineCode(context, code_stream, code_token,
+                       context.string_literal_values().Get(
+                           context.tokens().GetStringLiteralValue(code_token)));
       // TODO: Inject a clang pragma here to produce an error if there are
       // TODO: Inject a clang pragma here to produce an error if there are
       // unclosed scopes at the end of this inline C++ fragment.
       // unclosed scopes at the end of this inline C++ fragment.
     } else if (import.library_id.has_value()) {
     } else if (import.library_id.has_value()) {
@@ -90,7 +96,7 @@ static auto GenerateCppIncludesHeaderCode(
       }
       }
     }
     }
   }
   }
-  return code;
+  return code_stream.TakeStr();
 }
 }
 
 
 // Adds the given source location and an `ImportIRInst` referring to it in
 // Adds the given source location and an `ImportIRInst` referring to it in
@@ -347,6 +353,8 @@ class CarbonExternalASTSource : public clang::ExternalASTSource {
   bool root_scope_initialized_ = false;
   bool root_scope_initialized_ = false;
 };
 };
 
 
+}  // namespace
+
 void CarbonExternalASTSource::StartTranslationUnit(
 void CarbonExternalASTSource::StartTranslationUnit(
     clang::ASTConsumer* /*Consumer*/) {
     clang::ASTConsumer* /*Consumer*/) {
   auto& translation_unit = *ast_context_->getTranslationUnitDecl();
   auto& translation_unit = *ast_context_->getTranslationUnitDecl();
@@ -526,6 +534,33 @@ auto CarbonExternalASTSource::FindExternalVisibleDeclsByName(
   return true;
   return true;
 }
 }
 
 
+// Parses a sequence of top-level declarations and forms a corresponding
+// representation in the Clang AST. Unlike clang::ParseAST, does not finish the
+// translation unit when EOF is reached.
+static auto ParseTopLevelDecls(clang::Parser& parser,
+                               clang::ASTConsumer& consumer) -> void {
+  // Don't allow C++20 module declarations in inline Cpp code fragments.
+  auto module_import_state = clang::Sema::ModuleImportState::NotACXX20Module;
+
+  // Parse top-level declarations until we see EOF. Do not parse EOF, as that
+  // will cause the parser to end the translation unit prematurely.
+  while (parser.getCurToken().isNot(clang::tok::eof)) {
+    clang::Parser::DeclGroupPtrTy decl_group;
+    bool eof = parser.ParseTopLevelDecl(decl_group, module_import_state);
+    CARBON_CHECK(!eof, "Should not parse decls at EOF");
+    if (decl_group && !consumer.HandleTopLevelDecl(decl_group.get())) {
+      // If the consumer rejects the declaration, bail out of parsing.
+      //
+      // TODO: In this case, we shouldn't parse any more declarations even in
+      // separate inline C++ fragments. But our current AST consumer only ever
+      // returns true.
+      break;
+    }
+  }
+}
+
+namespace {
+
 // An action and a set of registered Clang callbacks used to generate an AST
 // An action and a set of registered Clang callbacks used to generate an AST
 // from a set of Cpp imports.
 // from a set of Cpp imports.
 class GenerateASTAction : public clang::ASTFrontendAction {
 class GenerateASTAction : public clang::ASTFrontendAction {
@@ -583,20 +618,7 @@ class GenerateASTAction : public clang::ASTFrontendAction {
     context_->set_cpp_context(
     context_->set_cpp_context(
         std::make_unique<CppContext>(clang_instance, std::move(parser_ptr)));
         std::make_unique<CppContext>(clang_instance, std::move(parser_ptr)));
 
 
-    // Don't allow C++20 module declarations in inline Cpp code fragments.
-    auto module_import_state = clang::Sema::ModuleImportState::NotACXX20Module;
-
-    // Parse top-level declarations until we see EOF. Do not parse EOF, as that
-    // will cause the parser to end the translation unit prematurely.
-    while (parser.getCurToken().isNot(clang::tok::eof)) {
-      clang::Parser::DeclGroupPtrTy decl_group;
-      bool eof = parser.ParseTopLevelDecl(decl_group, module_import_state);
-      CARBON_CHECK(!eof);
-      if (decl_group && !clang_instance.getASTConsumer().HandleTopLevelDecl(
-                            decl_group.get())) {
-        break;
-      }
-    }
+    ParseTopLevelDecls(parser, clang_instance.getASTConsumer());
   }
   }
 
 
  private:
  private:
@@ -686,6 +708,40 @@ auto GenerateAst(Context& context,
   return true;
   return true;
 }
 }
 
 
+auto InjectAstFromInlineCode(Context& context, SemIR::LocId loc_id,
+                             llvm::StringRef source_code) -> void {
+  auto* cpp_context = context.cpp_context();
+  CARBON_CHECK(cpp_context);
+
+  clang::Sema& sema = cpp_context->sema();
+  clang::Preprocessor& preprocessor = sema.getPreprocessor();
+  clang::Parser& parser = cpp_context->parser();
+
+  RawStringOstream code_stream;
+  AppendInlineCode(context, code_stream,
+                   context.parse_tree().node_token(loc_id.node_id()),
+                   source_code);
+
+  auto buffer = llvm::MemoryBuffer::getMemBufferCopy(code_stream.TakeStr(),
+                                                     "<inline c++>");
+  clang::FileID file_id =
+      preprocessor.getSourceManager().createFileID(std::move(buffer));
+
+  if (preprocessor.EnterSourceFile(file_id, nullptr, clang::SourceLocation())) {
+    // Clang will have generated a suitable error. There's nothing more to do
+    // here.
+    return;
+  }
+
+  // The parser will typically have an EOF as its cached current token; consume
+  // that so we can reach the newly-injected tokens.
+  if (parser.getCurToken().is(clang::tok::eof)) {
+    parser.ConsumeToken();
+  }
+
+  ParseTopLevelDecls(parser, sema.getASTConsumer());
+}
+
 auto FinishAst(Context& context) -> void {
 auto FinishAst(Context& context) -> void {
   if (!context.cpp_context()) {
   if (!context.cpp_context()) {
     return;
     return;

+ 10 - 0
toolchain/check/cpp/generate_ast.h

@@ -5,10 +5,15 @@
 #ifndef CARBON_TOOLCHAIN_CHECK_CPP_GENERATE_AST_H_
 #ifndef CARBON_TOOLCHAIN_CHECK_CPP_GENERATE_AST_H_
 #define CARBON_TOOLCHAIN_CHECK_CPP_GENERATE_AST_H_
 #define CARBON_TOOLCHAIN_CHECK_CPP_GENERATE_AST_H_
 
 
+#include <memory>
+
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/IntrusiveRefCntPtr.h"
 #include "llvm/ADT/IntrusiveRefCntPtr.h"
+#include "llvm/ADT/StringRef.h"
 #include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/Support/VirtualFileSystem.h"
 #include "toolchain/check/context.h"
 #include "toolchain/check/context.h"
+#include "toolchain/parse/tree.h"
+#include "toolchain/sem_ir/ids.h"
 
 
 namespace Carbon::Check {
 namespace Carbon::Check {
 
 
@@ -22,6 +27,11 @@ auto GenerateAst(Context& context,
                  std::shared_ptr<clang::CompilerInvocation> base_invocation)
                  std::shared_ptr<clang::CompilerInvocation> base_invocation)
     -> bool;
     -> bool;
 
 
+// Injects C++ code from `inline Cpp` into the active Clang AST context.
+// Returns a bool representing whether parsing was successful.
+auto InjectAstFromInlineCode(Context& context, SemIR::LocId loc_id,
+                             llvm::StringRef source_code) -> void;
+
 // Finishes AST generation for the given checking context. Performs end of file
 // Finishes AST generation for the given checking context. Performs end of file
 // steps such as template instantiation and warning on unused declarations.
 // steps such as template instantiation and warning on unused declarations.
 auto FinishAst(Context& context) -> void;
 auto FinishAst(Context& context) -> void;

+ 3 - 2
toolchain/check/handle_import_and_package.cpp

@@ -99,8 +99,9 @@ auto HandleParseNode(Context& /*context*/,
   return true;
   return true;
 }
 }
 
 
-auto HandleParseNode(Context& /*context*/,
-                     Parse::InlineImportBodyId /*node_id*/) -> bool {
+auto HandleParseNode(Context& context, Parse::InlineImportBodyId node_id)
+    -> bool {
+  context.node_stack().Push(node_id);
   return true;
   return true;
 }
 }
 
 

+ 56 - 0
toolchain/check/handle_inline_decl.cpp

@@ -0,0 +1,56 @@
+// 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 "common/check.h"
+#include "toolchain/check/context.h"
+#include "toolchain/check/cpp/generate_ast.h"
+#include "toolchain/check/diagnostic_helpers.h"
+#include "toolchain/check/handle.h"
+#include "toolchain/check/inst.h"
+#include "toolchain/lex/token_kind.h"
+#include "toolchain/parse/node_ids.h"
+#include "toolchain/parse/typed_nodes.h"
+#include "toolchain/sem_ir/typed_insts.h"
+
+namespace Carbon::Check {
+
+auto HandleParseNode(Context& /*context*/,
+                     Parse::InlineIntroducerId /*node_id*/) -> bool {
+  return true;
+}
+
+auto HandleParseNode(Context& context, Parse::InlineCppDeclId node_id) -> bool {
+  auto body_id = context.node_stack()
+                     .PopForSoloNodeId<Parse::NodeKind::InlineImportBody>();
+
+  // If there are no Cpp imports prior to this point, the `Cpp` expression after
+  // `import` will have already produced an error.
+  //
+  // TODO: It'd be nice to produce a clearer error saying to insert an `import
+  // Cpp` in a file that uses `inline Cpp` and doesn't otherwise import anything
+  // from package `Cpp`.
+  if (context.constant_values().Get(
+          context.node_stack().Pop<Parse::NodeKind::CppNameExpr>()) ==
+      SemIR::ErrorInst::ConstantId) {
+    return true;
+  }
+  CARBON_CHECK(context.cpp_context(), "Have `Cpp` name but no Cpp context");
+
+  auto string_token = context.parse_tree().node_token(body_id);
+  auto string_value_id = context.tokens().GetStringLiteralValue(string_token);
+  auto string_value = context.string_literal_values().Get(string_value_id);
+
+  if (context.scope_stack().PeekIndex() != ScopeIndex::Package) {
+    CARBON_DIAGNOSTIC(InlineDeclNotAtFileScope, Error,
+                      "`inline Cpp` declaration not at file scope");
+    context.emitter().Emit(node_id, InlineDeclNotAtFileScope);
+    return true;
+  }
+
+  InjectAstFromInlineCode(context, SemIR::LocId(body_id), string_value);
+  AddInst<SemIR::InlineCppDecl>(context, node_id, {.text_id = string_value_id});
+  return true;
+}
+
+}  // namespace Carbon::Check

+ 2 - 1
toolchain/check/node_stack.h

@@ -478,6 +478,8 @@ class NodeStack {
       case Parse::NodeKind::TuplePatternStart:
       case Parse::NodeKind::TuplePatternStart:
       case Parse::NodeKind::VariableInitializer:
       case Parse::NodeKind::VariableInitializer:
       case Parse::NodeKind::VariableIntroducer:
       case Parse::NodeKind::VariableIntroducer:
+      case Parse::NodeKind::InlineImportBody:
+      case Parse::NodeKind::InlineIntroducer:
         return Id::Kind::None;
         return Id::Kind::None;
       case Parse::NodeKind::AdaptIntroducer:
       case Parse::NodeKind::AdaptIntroducer:
       case Parse::NodeKind::AliasInitializer:
       case Parse::NodeKind::AliasInitializer:
@@ -511,7 +513,6 @@ class NodeStack {
       case Parse::NodeKind::LibraryIntroducer:
       case Parse::NodeKind::LibraryIntroducer:
       case Parse::NodeKind::LibrarySpecifier:
       case Parse::NodeKind::LibrarySpecifier:
       case Parse::NodeKind::InlineImportSpecifier:
       case Parse::NodeKind::InlineImportSpecifier:
-      case Parse::NodeKind::InlineImportBody:
       case Parse::NodeKind::MatchCase:
       case Parse::NodeKind::MatchCase:
       case Parse::NodeKind::MatchCaseGuard:
       case Parse::NodeKind::MatchCaseGuard:
       case Parse::NodeKind::MatchCaseGuardIntroducer:
       case Parse::NodeKind::MatchCaseGuardIntroducer:

+ 141 - 0
toolchain/check/testdata/interop/cpp/inline_decl.carbon

@@ -0,0 +1,141 @@
+// 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/none.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/interop/cpp/inline_decl.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/interop/cpp/inline_decl.carbon
+
+// --- inline_decl.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp;
+
+inline Cpp '''
+void func1() {}
+''';
+
+fn Run1() {
+  Cpp.func1();
+}
+
+inline Cpp '''
+void func2() {
+  Carbon::Run1();
+}
+''';
+
+fn Run2() {
+  Cpp.func2();
+}
+
+// --- fail_redefinition.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp;
+
+inline Cpp '''
+void f() {}
+''';
+
+inline Cpp '''
+// CHECK:STDERR: fail_redefinition.carbon:[[@LINE+7]]:6: error: redefinition of 'f' [CppInteropParseError]
+// CHECK:STDERR:    18 | void f() {}
+// CHECK:STDERR:       |      ^
+// CHECK:STDERR: fail_redefinition.carbon:[[@LINE-7]]:6: note: previous definition is here [CppInteropParseNote]
+// CHECK:STDERR:     7 | void f() {}
+// CHECK:STDERR:       |      ^
+// CHECK:STDERR:
+void f() {}
+''';
+
+// --- fail_diag_location.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp;
+
+// CHECK:STDERR: fail_diag_location.carbon:[[@LINE+4]]:12: error: use of undeclared identifier 'undeclared' [CppInteropParseError]
+// CHECK:STDERR:    10 | void f() { undeclared = 0; }
+// CHECK:STDERR:       |            ^~~~~~~~~~
+// CHECK:STDERR:
+inline Cpp "void f() { undeclared = 0; }";
+
+inline Cpp '''
+void g() {
+  // CHECK:STDERR: fail_diag_location.carbon:[[@LINE+4]]:3: error: use of undeclared identifier 'undeclared' [CppInteropParseError]
+  // CHECK:STDERR:    18 |   undeclared = 0;
+  // CHECK:STDERR:       |   ^~~~~~~~~~
+  // CHECK:STDERR:
+  undeclared = 0;
+}
+''';
+
+// --- fail_inline_no_import.carbon
+
+library "[[@TEST_NAME]]";
+
+// CHECK:STDERR: fail_inline_no_import.carbon:[[@LINE+4]]:8: error: name `Cpp` not found [NameNotFound]
+// CHECK:STDERR: inline Cpp "";
+// CHECK:STDERR:        ^~~
+// CHECK:STDERR:
+inline Cpp "";
+
+// --- fail_nested.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp;
+
+class C {
+  // CHECK:STDERR: fail_nested.carbon:[[@LINE+4]]:3: error: `inline Cpp` declaration not at file scope [InlineDeclNotAtFileScope]
+  // CHECK:STDERR:   inline Cpp "";
+  // CHECK:STDERR:   ^~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  inline Cpp "";
+}
+
+interface I {
+  // CHECK:STDERR: fail_nested.carbon:[[@LINE+4]]:3: error: `inline Cpp` declaration not at file scope [InlineDeclNotAtFileScope]
+  // CHECK:STDERR:   inline Cpp "";
+  // CHECK:STDERR:   ^~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  inline Cpp "";
+}
+
+constraint N {
+  // CHECK:STDERR: fail_nested.carbon:[[@LINE+4]]:3: error: `inline Cpp` declaration not at file scope [InlineDeclNotAtFileScope]
+  // CHECK:STDERR:   inline Cpp "";
+  // CHECK:STDERR:   ^~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  inline Cpp "";
+}
+
+impl C as I {
+  // CHECK:STDERR: fail_nested.carbon:[[@LINE+4]]:3: error: `inline Cpp` declaration not at file scope [InlineDeclNotAtFileScope]
+  // CHECK:STDERR:   inline Cpp "";
+  // CHECK:STDERR:   ^~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  inline Cpp "";
+}
+
+// --- fail_nested_fn.carbon
+
+fn F() {
+  // TODO: Should this be a check error rather than a parse error?
+  // CHECK:STDERR: fail_nested_fn.carbon:[[@LINE+8]]:3: error: expected expression [ExpectedExpr]
+  // CHECK:STDERR:   inline Cpp "";
+  // CHECK:STDERR:   ^~~~~~
+  // CHECK:STDERR:
+  // CHECK:STDERR: fail_nested_fn.carbon:[[@LINE+4]]:3: error: semantics TODO: `handle invalid parse trees in `check`` [SemanticsTodo]
+  // CHECK:STDERR:   inline Cpp "";
+  // CHECK:STDERR:   ^~~~~~
+  // CHECK:STDERR:
+  inline Cpp "";
+}

+ 5 - 0
toolchain/diagnostics/kind.def

@@ -131,6 +131,11 @@ CARBON_DIAGNOSTIC_KIND(UnexpectedTokenAfterListElement)
 CARBON_DIAGNOSTIC_KIND(UnrecognizedDecl)
 CARBON_DIAGNOSTIC_KIND(UnrecognizedDecl)
 CARBON_DIAGNOSTIC_KIND(UnexpectedTokenInCompoundMemberAccess)
 CARBON_DIAGNOSTIC_KIND(UnexpectedTokenInCompoundMemberAccess)
 
 
+// Inline declaration diagnostics.
+CARBON_DIAGNOSTIC_KIND(ExpectedCppAfterInline)
+CARBON_DIAGNOSTIC_KIND(ExpectedStringAfterInlineCpp)
+CARBON_DIAGNOSTIC_KIND(InlineDeclNotAtFileScope)
+
 // Match diagnostics.
 // Match diagnostics.
 CARBON_DIAGNOSTIC_KIND(ExpectedMatchCaseArrow)
 CARBON_DIAGNOSTIC_KIND(ExpectedMatchCaseArrow)
 CARBON_DIAGNOSTIC_KIND(ExpectedMatchCaseBlock)
 CARBON_DIAGNOSTIC_KIND(ExpectedMatchCaseBlock)

+ 84 - 0
toolchain/lower/testdata/interop/cpp/reverse/function.carbon

@@ -24,6 +24,28 @@ void G() {
 }
 }
 ''';
 ''';
 
 
+// --- single_file.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp;
+
+fn F() {}
+
+inline Cpp '''
+inline void G1() {
+  Carbon::F();
+}
+
+// Call an inline function indirectly to ensure declarations are getting
+// properly registered with the CodeGen consumer.
+inline void G2() {
+  G1();
+}
+''';
+
+fn H() { Cpp.G2(); }
+
 // CHECK:STDOUT: ; ModuleID = 'other.carbon'
 // CHECK:STDOUT: ; ModuleID = 'other.carbon'
 // CHECK:STDOUT: source_filename = "other.carbon"
 // CHECK:STDOUT: source_filename = "other.carbon"
 // CHECK:STDOUT:
 // CHECK:STDOUT:
@@ -78,3 +100,65 @@ void G() {
 // CHECK:STDOUT: !8 = !{!"int", !9, i64 0}
 // CHECK:STDOUT: !8 = !{!"int", !9, i64 0}
 // CHECK:STDOUT: !9 = !{!"omnipotent char", !10, i64 0}
 // CHECK:STDOUT: !9 = !{!"omnipotent char", !10, i64 0}
 // CHECK:STDOUT: !10 = !{!"Simple C++ TBAA"}
 // CHECK:STDOUT: !10 = !{!"Simple C++ TBAA"}
+// CHECK:STDOUT: ; ModuleID = 'single_file.carbon'
+// CHECK:STDOUT: source_filename = "single_file.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: $_Z2G2v = comdat any
+// CHECK:STDOUT:
+// CHECK:STDOUT: $_Z2G1v = comdat any
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CF.Main() #0 !dbg !11 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CH.Main() #0 !dbg !15 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   call void @_Z2G2v(), !dbg !16
+// CHECK:STDOUT:   ret void, !dbg !17
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: inlinehint mustprogress uwtable
+// CHECK:STDOUT: define linkonce_odr dso_local void @_Z2G2v() #1 comdat {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   call void @_Z2G1v()
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: inlinehint mustprogress nounwind uwtable
+// CHECK:STDOUT: define linkonce_odr dso_local void @_Z2G1v() #2 comdat {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   call void @_CF.Main()
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { inlinehint 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 #2 = { inlinehint 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: "single_file.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 = distinct !DISubprogram(name: "F", linkageName: "_CF.Main", scope: null, file: !6, line: 6, type: !12, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !12 = !DISubroutineType(types: !13)
+// CHECK:STDOUT: !13 = !{null}
+// CHECK:STDOUT: !14 = !DILocation(line: 6, column: 1, scope: !11)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "H", linkageName: "_CH.Main", scope: null, file: !6, line: 20, type: !12, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !16 = !DILocation(line: 20, column: 10, scope: !15)
+// CHECK:STDOUT: !17 = !DILocation(line: 20, column: 1, scope: !15)

+ 4 - 0
toolchain/parse/handle_decl_scope_loop.cpp

@@ -147,6 +147,9 @@ static constexpr auto DeclIntroducers = [] {
   set_contextual(Lex::TokenKind::Var, ClassContext, NodeKind::FieldIntroducer,
   set_contextual(Lex::TokenKind::Var, ClassContext, NodeKind::FieldIntroducer,
                  StateKind::FieldDecl);
                  StateKind::FieldDecl);
 
 
+  set(Lex::TokenKind::Inline, NodeKind::InlineIntroducer,
+      StateKind::InlineDeclAfterIntroducer);
+
   set_packaging(Lex::TokenKind::Package, NodeKind::PackageIntroducer,
   set_packaging(Lex::TokenKind::Package, NodeKind::PackageIntroducer,
                 StateKind::Package);
                 StateKind::Package);
   set_packaging(Lex::TokenKind::Library, NodeKind::LibraryIntroducer,
   set_packaging(Lex::TokenKind::Library, NodeKind::LibraryIntroducer,
@@ -226,6 +229,7 @@ static auto ResolveAmbiguousTokenAsDeclaration(Context& context,
         case Lex::TokenKind::Extern:
         case Lex::TokenKind::Extern:
         case Lex::TokenKind::Fn:
         case Lex::TokenKind::Fn:
         case Lex::TokenKind::Import:
         case Lex::TokenKind::Import:
+        case Lex::TokenKind::Inline:
         case Lex::TokenKind::Interface:
         case Lex::TokenKind::Interface:
         case Lex::TokenKind::Let:
         case Lex::TokenKind::Let:
         case Lex::TokenKind::Library:
         case Lex::TokenKind::Library:

+ 43 - 0
toolchain/parse/handle_inline_decl.cpp

@@ -0,0 +1,43 @@
+// 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/lex/token_kind.h"
+#include "toolchain/parse/context.h"
+#include "toolchain/parse/handle.h"
+#include "toolchain/parse/node_kind.h"
+#include "toolchain/parse/state.h"
+
+namespace Carbon::Parse {
+
+auto HandleInlineDeclAfterIntroducer(Context& context) -> void {
+  auto state = context.PopState();
+
+  if (auto cpp_token = context.ConsumeIf(Lex::TokenKind::Cpp)) {
+    context.AddLeafNode(NodeKind::CppNameExpr, *cpp_token);
+  } else {
+    CARBON_DIAGNOSTIC(ExpectedCppAfterInline, Error,
+                      "expected `Cpp` after `inline`");
+    context.emitter().Emit(*context.position(), ExpectedCppAfterInline);
+    context.AddNode(NodeKind::InlineCppDecl,
+                    context.SkipPastLikelyEnd(state.token),
+                    /*has_error=*/true);
+    return;
+  }
+
+  if (auto str = context.ConsumeIf(Lex::TokenKind::StringLiteral)) {
+    context.AddLeafNode(NodeKind::InlineImportBody, *str);
+    context.AddNodeExpectingDeclSemi(state, NodeKind::InlineCppDecl,
+                                     Lex::TokenKind::Inline,
+                                     /*is_def_allowed=*/false);
+  } else {
+    CARBON_DIAGNOSTIC(ExpectedStringAfterInlineCpp, Error,
+                      "expected string literal after `inline Cpp`");
+    context.emitter().Emit(*context.position(), ExpectedStringAfterInlineCpp);
+    context.AddNode(NodeKind::InlineCppDecl,
+                    context.SkipPastLikelyEnd(state.token),
+                    /*has_error=*/true);
+  }
+}
+
+}  // namespace Carbon::Parse

+ 3 - 0
toolchain/parse/node_kind.def

@@ -130,6 +130,9 @@ CARBON_PARSE_NODE_KIND(LibrarySpecifier)
 CARBON_PARSE_NODE_KIND(InlineImportSpecifier)
 CARBON_PARSE_NODE_KIND(InlineImportSpecifier)
 CARBON_PARSE_NODE_KIND(InlineImportBody)
 CARBON_PARSE_NODE_KIND(InlineImportBody)
 
 
+CARBON_PARSE_NODE_KIND(InlineIntroducer)
+CARBON_PARSE_NODE_KIND(InlineCppDecl)
+
 CARBON_PARSE_NODE_KIND(IdentifierNameQualifierWithParams)
 CARBON_PARSE_NODE_KIND(IdentifierNameQualifierWithParams)
 CARBON_PARSE_NODE_KIND(IdentifierNameQualifierWithoutParams)
 CARBON_PARSE_NODE_KIND(IdentifierNameQualifierWithoutParams)
 
 

+ 11 - 0
toolchain/parse/state.def

@@ -368,6 +368,10 @@ CARBON_PARSE_STATE(DeclNameAndParamsAfterParams)
 // ^~~~~~
 // ^~~~~~
 //   1. Import
 //   1. Import
 //
 //
+// inline ...
+// ^~~~~~
+//   1. InlineDeclAfterIntroducer (variant is Regular)
+//
 // interface ...
 // interface ...
 // ^~~~~~~~~
 // ^~~~~~~~~
 //   1. TypeAfterIntroducerAsInterface
 //   1. TypeAfterIntroducerAsInterface
@@ -433,6 +437,13 @@ CARBON_PARSE_STATE_VARIANTS3(Decl, Class, Interface, Regular)
 //
 //
 CARBON_PARSE_STATE_VARIANTS3(DeclScopeLoop, Class, Interface, Regular)
 CARBON_PARSE_STATE_VARIANTS3(DeclScopeLoop, Class, Interface, Regular)
 
 
+// Handles processing of an inline declaration.
+//
+// inline Cpp "..." ;
+//        ^~~~~~~~~~~
+//   (state done)
+CARBON_PARSE_STATE(InlineDeclAfterIntroducer)
+
 // Handles periods. Only does one `.<expression>` segment; the source is
 // Handles periods. Only does one `.<expression>` segment; the source is
 // responsible for handling chaining.
 // responsible for handling chaining.
 //
 //

+ 85 - 0
toolchain/parse/testdata/packages/import/cpp_inline.carbon

@@ -72,6 +72,52 @@ import Cpp inline '''
 int n;
 int n;
 ''';
 ''';
 
 
+// --- inline_cpp.carbon
+
+inline Cpp '''
+int n;
+''';
+
+// --- fail_inline_cpp_syntax_errors.carbon
+
+// CHECK:STDERR: fail_inline_cpp_syntax_errors.carbon:[[@LINE+4]]:7: error: expected `Cpp` after `inline` [ExpectedCppAfterInline]
+// CHECK:STDERR: inline;
+// CHECK:STDERR:       ^
+// CHECK:STDERR:
+inline;
+
+// CHECK:STDERR: fail_inline_cpp_syntax_errors.carbon:[[@LINE+4]]:8: error: expected `Cpp` after `inline` [ExpectedCppAfterInline]
+// CHECK:STDERR: inline '''
+// CHECK:STDERR:        ^~~
+// CHECK:STDERR:
+inline '''
+int n;
+''';
+
+// CHECK:STDERR: fail_inline_cpp_syntax_errors.carbon:[[@LINE+4]]:11: error: expected string literal after `inline Cpp` [ExpectedStringAfterInlineCpp]
+// CHECK:STDERR: inline Cpp;
+// CHECK:STDERR:           ^
+// CHECK:STDERR:
+inline Cpp;
+
+// CHECK:STDERR: fail_inline_cpp_syntax_errors.carbon:[[@LINE+4]]:12: error: expected string literal after `inline Cpp` [ExpectedStringAfterInlineCpp]
+// CHECK:STDERR: inline Cpp x;
+// CHECK:STDERR:            ^
+// CHECK:STDERR:
+inline Cpp x;
+
+inline Cpp '''
+int n;
+'''
+// CHECK:STDERR: fail_inline_cpp_syntax_errors.carbon:[[@LINE+4]]:1: error: `inline` declarations must end with a `;` [ExpectedDeclSemi]
+// CHECK:STDERR:
+// CHECK:STDERR: ^
+// CHECK:STDERR:
+
+// --- eof.carbon
+// This split exists to avoid the "no `;` at EOF" diagnostic above pointing to
+// the last CHECK:STDOUT line below.
+
 // CHECK:STDOUT: - filename: fail_bad_syntax.carbon
 // CHECK:STDOUT: - filename: fail_bad_syntax.carbon
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
@@ -150,3 +196,42 @@ int n;
 // CHECK:STDOUT:     {kind: 'ImportDecl', text: ';', subtree_size: 5},
 // CHECK:STDOUT:     {kind: 'ImportDecl', text: ';', subtree_size: 5},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]
 // CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: inline_cpp.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'InlineIntroducer', text: 'inline'},
+// CHECK:STDOUT:       {kind: 'CppNameExpr', text: 'Cpp'},
+// CHECK:STDOUT:       {kind: 'InlineImportBody', text: ''''
+// CHECK:STDOUT: int n;
+// CHECK:STDOUT: ''''},
+// CHECK:STDOUT:     {kind: 'InlineCppDecl', text: ';', subtree_size: 4},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: fail_inline_cpp_syntax_errors.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'InlineIntroducer', text: 'inline'},
+// CHECK:STDOUT:     {kind: 'InlineCppDecl', text: ';', has_error: yes, subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'InlineIntroducer', text: 'inline'},
+// CHECK:STDOUT:     {kind: 'InlineCppDecl', text: ';', has_error: yes, subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'InlineIntroducer', text: 'inline'},
+// CHECK:STDOUT:       {kind: 'CppNameExpr', text: 'Cpp'},
+// CHECK:STDOUT:     {kind: 'InlineCppDecl', text: ';', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'InlineIntroducer', text: 'inline'},
+// CHECK:STDOUT:       {kind: 'CppNameExpr', text: 'Cpp'},
+// CHECK:STDOUT:     {kind: 'InlineCppDecl', text: ';', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'InlineIntroducer', text: 'inline'},
+// CHECK:STDOUT:       {kind: 'CppNameExpr', text: 'Cpp'},
+// CHECK:STDOUT:       {kind: 'InlineImportBody', text: ''''
+// CHECK:STDOUT: int n;
+// CHECK:STDOUT: ''''},
+// CHECK:STDOUT:     {kind: 'InlineCppDecl', text: ''''
+// CHECK:STDOUT: int n;
+// CHECK:STDOUT: '''', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: eof.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 12 - 0
toolchain/parse/typed_nodes.h

@@ -251,6 +251,18 @@ struct InlineImportSpecifier {
   InlineImportBodyId body;
   InlineImportBodyId body;
 };
 };
 
 
+using InlineIntroducer =
+    LeafNode<NodeKind::InlineIntroducer, Lex::InlineTokenIndex>;
+struct InlineCppDecl {
+  static constexpr auto Kind = NodeKind::InlineCppDecl.Define(
+      {.category = NodeCategory::Decl, .bracketed_by = InlineIntroducer::Kind});
+
+  InlineIntroducerId introducer;
+  CppNameExprId cpp_name;
+  InlineImportBodyId body;
+  Lex::SemiTokenIndex token;
+};
+
 // First line of the file, such as:
 // First line of the file, such as:
 //   `impl package MyPackage library "MyLibrary";`
 //   `impl package MyPackage library "MyLibrary";`
 struct PackageDecl {
 struct PackageDecl {

+ 2 - 1
toolchain/sem_ir/inst_kind.def

@@ -90,8 +90,9 @@ CARBON_SEM_IR_INST_KIND(ImportCppDecl)
 CARBON_SEM_IR_INST_KIND(ImportDecl)
 CARBON_SEM_IR_INST_KIND(ImportDecl)
 CARBON_SEM_IR_INST_KIND(ImportRefLoaded)
 CARBON_SEM_IR_INST_KIND(ImportRefLoaded)
 CARBON_SEM_IR_INST_KIND(ImportRefUnloaded)
 CARBON_SEM_IR_INST_KIND(ImportRefUnloaded)
-CARBON_SEM_IR_INST_KIND(InitForm)
 CARBON_SEM_IR_INST_KIND(InPlaceInit)
 CARBON_SEM_IR_INST_KIND(InPlaceInit)
+CARBON_SEM_IR_INST_KIND(InitForm)
+CARBON_SEM_IR_INST_KIND(InlineCppDecl)
 CARBON_SEM_IR_INST_KIND(InstType)
 CARBON_SEM_IR_INST_KIND(InstType)
 CARBON_SEM_IR_INST_KIND(InstValue)
 CARBON_SEM_IR_INST_KIND(InstValue)
 CARBON_SEM_IR_INST_KIND(IntLiteralType)
 CARBON_SEM_IR_INST_KIND(IntLiteralType)

+ 10 - 0
toolchain/sem_ir/typed_insts.h

@@ -1049,6 +1049,16 @@ struct ImportCppDecl {
            .is_lowered = false});
            .is_lowered = false});
 };
 };
 
 
+// An `inline Cpp` declaration.
+struct InlineCppDecl {
+  static constexpr auto Kind =
+      InstKind::InlineCppDecl.Define<Parse::InlineCppDeclId>(
+          {.ir_name = "inline_cpp",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
+  StringLiteralValueId text_id;
+};
+
 // An `import` declaration. This is mainly for `import` diagnostics, and a 1:1
 // An `import` declaration. This is mainly for `import` diagnostics, and a 1:1
 // correspondence with actual `import`s isn't guaranteed.
 // correspondence with actual `import`s isn't guaranteed.
 struct ImportDecl {
 struct ImportDecl {