From 6f1f59a385a64fb0744993e0cc0e9512bb7a6c1a Mon Sep 17 00:00:00 2001 From: David Blaikie Date: Fri, 13 Mar 2026 16:04:49 -0700 Subject: [PATCH] Initial Reverse Interop implementation (#6901) Add a clang::ExternalASTSource to begin exposing Carbon entities to Clang - initially only a single `Carbon` top level namespace. Subsequent work will add Carbon entities to this namespace. Likely this CarbonExternalASTSource will be refactored into another file, tie into/reference SemIR::File and CppFile, etc eventually - but that'll wait for future patches. If there's mechanical problems with the current implementation - how I'm creating the new NamespaceDecl, etc - I'm all ears. It's very much in the "it seems to work" state, not much more than that. This does break Clang Modules (header modules, C++20 modules, precompiled headers, etc) since they're implemented as an ExternalASTSource as well, and Clang's ASTContext only supports one ExternalASTSource at a time. To fix that regression we'll need to implement some kind of ExternalASTSource multiplexing support - either in Clang or Carbon (unclear which). This regression of modules support can be observed by the following: `A.h` ``` inline void f1() { } ``` `module.modulemap` ``` module A { header "A.h" export * } ``` `test.carbon` ``` import Cpp inline ''' // Hardcode the pragma to ensure this isn't silently falling back to // textual inclusion. void f2() { f1(); } '''; ``` ``` carbon compile test.carbon -- -I . -fmodules -fimplicit-modules -fmodules-cache-path=module_cache ``` I wrote a `file_test` test for this, but it doesn't /quite/ work because `file_test` provides an in-memory filesystem for tests to make them more hermetic, but Clang's Filesystem abstrtaction is for reading only - so the module that's written out successfully can't be found when it needs to be read back in - so the test doesn't pass as a baseline. Clang does have support for `llvm::vfs::OutputBackend` which allows virtualizing output - which I guess we could tie together with the InMemoryFilesystem we use for input to make such a test work. But I guess that's not worth the effort here? --------- Co-authored-by: Richard Smith Co-authored-by: Jon Ross-Perkins --- toolchain/check/cpp/generate_ast.cpp | 62 ++++++++++++++++++- .../interop/cpp/reverse/simple.carbon | 25 ++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 toolchain/check/testdata/interop/cpp/reverse/simple.carbon diff --git a/toolchain/check/cpp/generate_ast.cpp b/toolchain/check/cpp/generate_ast.cpp index 2e8acefe6cb9..f8e06e55cd78 100644 --- a/toolchain/check/cpp/generate_ast.cpp +++ b/toolchain/check/cpp/generate_ast.cpp @@ -17,6 +17,7 @@ #include "clang/Lex/PreprocessorOptions.h" #include "clang/Parse/Parser.h" #include "clang/Sema/ExternalSemaSource.h" +#include "clang/Sema/MultiplexExternalSemaSource.h" #include "clang/Sema/Sema.h" #include "common/check.h" #include "common/raw_string_ostream.h" @@ -331,6 +332,56 @@ class ShallowCopyCompilerInvocation : public clang::CompilerInvocation { } }; +class CarbonExternalASTSource : public clang::ExternalASTSource { + public: + explicit CarbonExternalASTSource(clang::ASTContext* ast_context) + : ast_context_(*ast_context) {} + + auto FindExternalVisibleDeclsByName( + const clang::DeclContext* decl_context, clang::DeclarationName decl_name, + const clang::DeclContext* original_decl_context) -> bool override; + + auto StartTranslationUnit(clang::ASTConsumer* consumer) -> void override; + + private: + clang::ASTContext& ast_context_; +}; + +void CarbonExternalASTSource::StartTranslationUnit( + clang::ASTConsumer* /*Consumer*/) { + auto& translation_unit = *ast_context_.getTranslationUnitDecl(); + // Mark the translation unit as having external storage so we get a query for + // the `Carbon` namespace in the top level/translation unit scope. + translation_unit.setHasExternalVisibleStorage(); +} + +auto CarbonExternalASTSource::FindExternalVisibleDeclsByName( + const clang::DeclContext* decl_context, clang::DeclarationName decl_name, + const clang::DeclContext* /*OriginalDC*/) -> bool { + if (decl_context->getDeclKind() != clang::Decl::Kind::TranslationUnit) { + return false; + } + + static const llvm::StringLiteral carbon_namespace_name = "Carbon"; + if (auto* identifier = decl_name.getAsIdentifierInfo(); + !identifier || !identifier->isStr(carbon_namespace_name)) { + return false; + } + + auto& ast_context = decl_context->getParentASTContext(); + auto& mutable_tu_decl_context = *ast_context.getTranslationUnitDecl(); + SetExternalVisibleDeclsForName( + decl_context, decl_name, + { + clang::NamespaceDecl::Create( + ast_context, &mutable_tu_decl_context, false, + clang::SourceLocation(), clang::SourceLocation(), + &ast_context.Idents.get(carbon_namespace_name), nullptr, false), + }); + + return true; +} + // An action and a set of registered Clang callbacks used to generate an AST // from a set of Cpp imports. class GenerateASTAction : public clang::ASTFrontendAction { @@ -358,8 +409,6 @@ class GenerateASTAction : public clang::ASTFrontendAction { auto BeginSourceFileAction(clang::CompilerInstance& /*clang_instance*/) -> bool override { - // TODO: Consider creating an `ExternalSemaSource` here and attaching it to - // the compilation. // TODO: `clang.getPreprocessor().enableIncrementalProcessing();` to avoid // the TU scope getting torn down before we're done parsing macros. return true; @@ -470,6 +519,15 @@ auto GenerateAst(Context& context, return false; } + auto& ast = clang_instance.getASTContext(); + // TODO: Clang's modules support is implemented as an ExternalASTSource + // (ASTReader) and there's no multiplexing support for ExternalASTSources at + // the moment - so registering CarbonExternalASTSource breaks Clang modules + // support. Implement multiplexing support (possibly in Clang) to restore + // modules functionality. + ast.setExternalSource( + llvm::makeIntrusiveRefCnt(&ast)); + if (llvm::Error error = action.Execute()) { // `Execute` currently never fails, but its contract allows it to. context.TODO(SemIR::LocId::None, "failed to execute clang action: " + diff --git a/toolchain/check/testdata/interop/cpp/reverse/simple.carbon b/toolchain/check/testdata/interop/cpp/reverse/simple.carbon new file mode 100644 index 000000000000..edbd0bae3dda --- /dev/null +++ b/toolchain/check/testdata/interop/cpp/reverse/simple.carbon @@ -0,0 +1,25 @@ +// 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/reverse/simple.carbon +// TIP: To dump output, run: +// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/interop/cpp/reverse/simple.carbon + +// --- fail_simple.carbon + +library "[[@TEST_NAME]]"; + +// Demonstrate that the 'Carbon' namespace has been injected into the C++ compilation. + +import Cpp inline ''' +// CHECK:STDERR: fail_simple.carbon:[[@LINE+4]]:9: error: no type named 'NonExistent' in namespace 'Carbon' [CppInteropParseError] +// CHECK:STDERR: 11 | Carbon::NonExistent v; +// CHECK:STDERR: | ~~~~~~~~^ +// CHECK:STDERR: +Carbon::NonExistent v; +''';