diff --git a/src/syntax/dependency_graph.cpp b/src/syntax/dependency_graph.cpp index 9f9c8d37..05a581fe 100644 --- a/src/syntax/dependency_graph.cpp +++ b/src/syntax/dependency_graph.cpp @@ -513,6 +513,35 @@ et::task<> scan_impl(CompilationDatabase& cdb, } // Record module interface unit mapping. + // When the module declaration is inside a conditional directive + // (need_preprocess=true), fall back to scan_module_decl() which + // runs a lightweight preprocessor pass to resolve the actual + // module name. This only applies to source files (wave 0) since + // headers cannot contain module declarations. + if(scan_result.scan_result.need_preprocess && wave_num == 0) { + auto file_path = llvm::StringRef(scan_result.path); + auto contexts = + cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true}); + if(!contexts.empty()) { + auto& ctx = contexts[0]; + auto fallback = scan_module_decl(ctx.arguments, ctx.directory, /*content=*/{}); + if(!fallback.module_name.empty()) { + scan_result.scan_result.module_name = std::move(fallback.module_name); + scan_result.scan_result.is_interface_unit = fallback.is_interface_unit; + // Update cache so warm runs don't re-trigger fallback. + if(ext_cache) { + auto cache_it = ext_cache->scan_results.find(scan_result.path_id); + if(cache_it != ext_cache->scan_results.end()) { + cache_it->second.module_name = scan_result.scan_result.module_name; + cache_it->second.is_interface_unit = + scan_result.scan_result.is_interface_unit; + cache_it->second.need_preprocess = false; + } + } + } + } + } + if(scan_result.scan_result.is_interface_unit) { graph.add_module(scan_result.scan_result.module_name, scan_result.path_id); } diff --git a/src/syntax/scan.cpp b/src/syntax/scan.cpp index 16fa7283..1c11ac7a 100644 --- a/src/syntax/scan.cpp +++ b/src/syntax/scan.cpp @@ -375,6 +375,55 @@ ScanResult scan_precise(llvm::ArrayRef arguments, return result; } +ScanResult scan_module_decl(llvm::ArrayRef arguments, + llvm::StringRef directory, + llvm::StringRef content, + SharedScanCache* cache, + llvm::IntrusiveRefCntPtr vfs) { + ScanResult result; + + if(!vfs) { + vfs = llvm::vfs::createPhysicalFileSystem(); + } + + auto instance = create_scan_instance(arguments, directory, content, vfs); + if(!instance) { + return result; + } + + auto getter = std::make_unique(cache, instance->getFileManager()); + instance->setDependencyDirectivesGetter(std::move(getter)); + + if(!instance->createTarget()) { + return result; + } + + auto action = std::make_unique(); + + if(!action->BeginSourceFile(*instance, instance->getFrontendOpts().Inputs[0])) { + return result; + } + + // Instead of action->Execute() which processes the entire file, + // manually lex tokens and stop as soon as the module declaration is found. + auto& pp = instance->getPreprocessor(); + pp.EnterMainSourceFile(); + + clang::Token tok; + do { + pp.Lex(tok); + if(pp.isInNamedModule()) { + result.module_name = pp.getNamedModuleName(); + result.is_interface_unit = pp.isInNamedInterfaceUnit(); + break; + } + } while(tok.isNot(clang::tok::eof)); + + action->EndSourceFile(); + + return result; +} + std::uint32_t compute_preamble_bound(llvm::StringRef content) { auto result = compute_preamble_bounds(content); if(result.empty()) { diff --git a/src/syntax/scan.h b/src/syntax/scan.h index 09a74cd0..169b7807 100644 --- a/src/syntax/scan.h +++ b/src/syntax/scan.h @@ -80,6 +80,21 @@ ScanResult scan_precise(llvm::ArrayRef arguments, SharedScanCache* cache = nullptr, llvm::IntrusiveRefCntPtr vfs = nullptr); +/// Lightweight preprocessor-based fallback for resolving module declarations +/// inside conditional directives (#if/#ifdef). When the quick `scan()` detects +/// `need_preprocess=true`, this function runs clang's preprocessor to evaluate +/// the conditions and extract the actual module name. +/// +/// Much cheaper than `scan_precise()`: stops lexing as soon as the module +/// declaration is found, so it only processes the file preamble (global module +/// fragment + conditionals around the module declaration). Only populates +/// `module_name` and `is_interface_unit` in the returned ScanResult. +ScanResult scan_module_decl(llvm::ArrayRef arguments, + llvm::StringRef directory, + llvm::StringRef content = {}, + SharedScanCache* cache = nullptr, + llvm::IntrusiveRefCntPtr vfs = nullptr); + /// Compute preamble bound (moved from compile/preamble). std::uint32_t compute_preamble_bound(llvm::StringRef content); diff --git a/tests/unit/syntax/module_import_tests.cpp b/tests/unit/syntax/module_import_tests.cpp new file mode 100644 index 00000000..a89e434f --- /dev/null +++ b/tests/unit/syntax/module_import_tests.cpp @@ -0,0 +1,285 @@ +#include "module_scan_fixture.h" +#include "test/test.h" +#include "syntax/scan.h" + +namespace clice::testing { +namespace { + +// ============================================================================= +// scan_precise() — module import semantics +// ============================================================================= + +TEST_SUITE(ModuleImportScan) { + +TEST_CASE(NamedImport) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +import other; +)"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "other"); +} + +TEST_CASE(MultipleImports) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +import alpha; +import beta; +import gamma; +)"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + ASSERT_EQ(result.modules.size(), 3u); + EXPECT_EQ(result.modules[0], "alpha"); + EXPECT_EQ(result.modules[1], "beta"); + EXPECT_EQ(result.modules[2], "gamma"); +} + +TEST_CASE(DottedModuleImport) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +import std.io; +)"); + auto result = f.precise(); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "std.io"); +} + +// Partition import: clang returns the fully-qualified name "mylib:core" +// (owning module + ':' + partition name) as a single ModuleIdPath entry. +TEST_CASE(PartitionImport) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +import :core; +)"); + auto result = f.precise(); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "mylib:core"); +} + +// Export-import of a named module. +TEST_CASE(ExportImport) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +export import other; +)"); + auto result = f.precise(); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "other"); +} + +// Export-import of a partition. +TEST_CASE(ExportImportPartition) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +export import :core; +)"); + auto result = f.precise(); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "mylib:core"); +} + +// Implementation unit importing a named module. +TEST_CASE(ImplementationImport) { + ModuleScanFixture f("impl.cpp", R"( +module mylib; +import other; +)"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_FALSE(result.is_interface_unit); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "other"); +} + +// Implementation unit importing a partition of the same module. +TEST_CASE(ImplementationPartitionImport) { + ModuleScanFixture f("impl.cpp", R"( +module mylib; +import :utils; +)"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "mylib:utils"); +} + +// Multiple partition imports. +TEST_CASE(MultiplePartitionImports) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +export import :core; +import :utils; +import :io; +)"); + auto result = f.precise(); + ASSERT_EQ(result.modules.size(), 3u); + EXPECT_EQ(result.modules[0], "mylib:core"); + EXPECT_EQ(result.modules[1], "mylib:utils"); + EXPECT_EQ(result.modules[2], "mylib:io"); +} + +// Mixed named module imports and partition imports. +TEST_CASE(MixedNamedAndPartitionImports) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +import other; +export import :core; +import another.lib; +import :utils; +)"); + auto result = f.precise(); + ASSERT_EQ(result.modules.size(), 4u); + EXPECT_EQ(result.modules[0], "other"); + EXPECT_EQ(result.modules[1], "mylib:core"); + EXPECT_EQ(result.modules[2], "another.lib"); + EXPECT_EQ(result.modules[3], "mylib:utils"); +} + +// NOTE: Header unit imports (import
; / import "header";) are not +// tested here because they require actual header unit compilation support +// which clang's PreprocessOnlyAction doesn't provide in a VFS-only context. +// These would hang trying to resolve system headers. + +// GMF with imports. +TEST_CASE(GMFWithImport) { + ModuleScanFixture f("main.cppm", R"( +module; +#include "config.h" +export module mylib; +import dep; +)"); + f.add_file("config.h", "// config\n"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "dep"); +} + +// Mixed includes (from GMF) and imports (after module decl). +TEST_CASE(MixedIncludesAndImports) { + ModuleScanFixture f("main.cppm", R"( +module; +#include "legacy.h" +export module mylib; +import dep_a; +import dep_b; +export int f(); +)"); + f.add_file("legacy.h", "int legacy_func();\n"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + ASSERT_GE(result.includes.size(), 1u); + ASSERT_EQ(result.modules.size(), 2u); + EXPECT_EQ(result.modules[0], "dep_a"); + EXPECT_EQ(result.modules[1], "dep_b"); +} + +// No module — plain C++ file. +TEST_CASE(NoModule) { + ModuleScanFixture f("main.cpp", R"( +#include "header.h" +int main() { return 0; } +)"); + f.add_file("header.h", "int x;\n"); + auto result = f.precise(); + EXPECT_TRUE(result.module_name.empty()); + EXPECT_FALSE(result.is_interface_unit); + EXPECT_TRUE(result.modules.empty()); +} + +// Partition interface unit declaring and importing another partition. +TEST_CASE(PartitionInterfaceImportingPartition) { + ModuleScanFixture f("main.cppm", R"( +export module mylib:ui; +import :core; +)"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib:ui"); + EXPECT_TRUE(result.is_interface_unit); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "mylib:core"); +} + +// Partition implementation importing another partition. +TEST_CASE(PartitionImplImportingPartition) { + ModuleScanFixture f("impl.cpp", R"( +module mylib:detail; +import :core; +)"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib:detail"); + EXPECT_FALSE(result.is_interface_unit); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "mylib:core"); +} + +// Import target is a macro-expanded name. +// C++20 forbids object-like macros in module DECLARATIONS (export module M;), +// but clang's preprocessor expands macros in import declarations. +TEST_CASE(ImportMacroExpandedName) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +#define OTHER_MOD other +import OTHER_MOD; +)"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "other"); +} + +// Import target from a macro defined on the command line. +TEST_CASE(ImportMacroFromCommandLine) { + ModuleScanFixture f("main.cppm", + R"( +export module mylib; +import DEP_MOD; +)", + {"-DDEP_MOD=dependency"}); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "dependency"); +} + +// Import target from a macro defined in GMF header. +TEST_CASE(ImportMacroFromGMFHeader) { + ModuleScanFixture f("main.cppm", R"( +module; +#include "deps.h" +export module mylib; +import MY_DEP; +)"); + f.add_file("deps.h", "#define MY_DEP some_lib\n"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "some_lib"); +} + +// Import target from a macro defined in a header #included AFTER the module +// declaration (not in GMF). C++20 allows #include after module declarations — +// the preprocessor still processes them and any macros they define are visible +// to subsequent import declarations. +TEST_CASE(ImportMacroFromPostDeclInclude) { + ModuleScanFixture f("main.cppm", R"( +export module mylib; +#include "imports.h" +import MY_IMPORT; +)"); + f.add_file("imports.h", "#define MY_IMPORT dep\n"); + auto result = f.precise(); + EXPECT_EQ(result.module_name, "mylib"); + ASSERT_EQ(result.modules.size(), 1u); + EXPECT_EQ(result.modules[0], "dep"); +} + +}; // TEST_SUITE(ModuleImportScan) + +} // namespace +} // namespace clice::testing diff --git a/tests/unit/syntax/module_scan_fixture.h b/tests/unit/syntax/module_scan_fixture.h new file mode 100644 index 00000000..570f805e --- /dev/null +++ b/tests/unit/syntax/module_scan_fixture.h @@ -0,0 +1,49 @@ +#pragma once + +#include "test/test.h" +#include "syntax/scan.h" + +namespace clice::testing { + +/// Helper that sets up a TestVFS with a main file and optional extra files, +/// then calls the given scan function with standard C++20 arguments. +struct ModuleScanFixture { + llvm::IntrusiveRefCntPtr vfs = llvm::makeIntrusiveRefCnt(); + std::string main_path; + std::vector args; + + // Non-copyable/non-movable: args stores raw pointers (main_path.c_str()) + // that would dangle after copy/move. + ModuleScanFixture(const ModuleScanFixture&) = delete; + ModuleScanFixture& operator=(const ModuleScanFixture&) = delete; + ModuleScanFixture(ModuleScanFixture&&) = delete; + ModuleScanFixture& operator=(ModuleScanFixture&&) = delete; + + /// Create fixture with main file content and optional extra defines. + ModuleScanFixture(llvm::StringRef filename, + llvm::StringRef content, + std::initializer_list extra_args = {}) { + main_path = TestVFS::path(filename); + vfs->add(filename, content); + args.push_back("clang++"); + args.push_back("-std=c++20"); + for(auto a: extra_args) { + args.push_back(a); + } + args.push_back(main_path.c_str()); + } + + void add_file(llvm::StringRef name, llvm::StringRef content = "") { + vfs->add(name, content); + } + + ScanResult decl() { + return scan_module_decl(args, TestVFS::root(), {}, nullptr, vfs); + } + + ScanResult precise() { + return scan_precise(args, TestVFS::root(), {}, nullptr, vfs); + } +}; + +} // namespace clice::testing diff --git a/tests/unit/syntax/module_scan_tests.cpp b/tests/unit/syntax/module_scan_tests.cpp new file mode 100644 index 00000000..aa6202b6 --- /dev/null +++ b/tests/unit/syntax/module_scan_tests.cpp @@ -0,0 +1,250 @@ +#include "module_scan_fixture.h" +#include "test/test.h" +#include "syntax/scan.h" + +namespace clice::testing { +namespace { + +// ============================================================================= +// scan() — module declaration extraction (lexer-based, cppref coverage) +// ============================================================================= + +TEST_SUITE(ModuleScan) { + +// Primary module interface: export module M; +TEST_CASE(PrimaryModuleInterface) { + auto result = scan("export module mylib;"); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); + EXPECT_FALSE(result.need_preprocess); +} + +// Module implementation unit: module M; +TEST_CASE(ModuleImplementationUnit) { + auto result = scan("module mylib;"); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_FALSE(result.is_interface_unit); + EXPECT_FALSE(result.need_preprocess); +} + +// Dotted module name: export module std.io; +TEST_CASE(DottedModuleName) { + auto result = scan("export module std.io;"); + EXPECT_EQ(result.module_name, "std.io"); + EXPECT_TRUE(result.is_interface_unit); +} + +// Deeply dotted module name: export module a.b.c.d; +TEST_CASE(DeeplyDottedModuleName) { + auto result = scan("export module a.b.c.d;"); + EXPECT_EQ(result.module_name, "a.b.c.d"); + EXPECT_TRUE(result.is_interface_unit); +} + +// Module partition interface: export module M:P; +TEST_CASE(PartitionInterface) { + auto result = scan("export module mylib:core;"); + EXPECT_EQ(result.module_name, "mylib:core"); + EXPECT_TRUE(result.is_interface_unit); +} + +// Module partition implementation: module M:P; +TEST_CASE(PartitionImplementation) { + auto result = scan("module mylib:core;"); + EXPECT_EQ(result.module_name, "mylib:core"); + EXPECT_FALSE(result.is_interface_unit); +} + +// Dotted module name + partition: export module a.b:p; +TEST_CASE(DottedModuleWithPartition) { + auto result = scan("export module a.b:p;"); + EXPECT_EQ(result.module_name, "a.b:p"); + EXPECT_TRUE(result.is_interface_unit); +} + +// Global module fragment with includes before module declaration. +TEST_CASE(GlobalModuleFragmentWithIncludes) { + auto result = scan(R"( +module; +#include +#include "config.h" +export module mylib; +)"); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); + ASSERT_EQ(result.includes.size(), 2u); + EXPECT_EQ(result.includes[0].path, "stdlib.h"); + EXPECT_TRUE(result.includes[0].is_angled); + EXPECT_EQ(result.includes[1].path, "config.h"); + EXPECT_FALSE(result.includes[1].is_angled); +} + +// Conditional module declaration with #ifdef. +TEST_CASE(ConditionalModuleIfdef) { + auto result = scan(R"( +#ifdef USE_MODULES +export module mylib; +#endif +)"); + EXPECT_TRUE(result.module_name.empty()); + EXPECT_TRUE(result.need_preprocess); +} + +// Conditional module declaration with #if __cpp_modules. +TEST_CASE(ConditionalModuleCppModules) { + auto result = scan(R"( +#if __cpp_modules >= 201907L +export module mylib; +#endif +)"); + EXPECT_TRUE(result.module_name.empty()); + EXPECT_TRUE(result.need_preprocess); +} + +// Conditional module declaration in global module fragment. +TEST_CASE(ConditionalModuleInGMF) { + auto result = scan(R"( +module; +#include +#ifdef USE_MODULES +export module mylib; +#endif +)"); + EXPECT_TRUE(result.module_name.empty()); + EXPECT_TRUE(result.need_preprocess); + ASSERT_EQ(result.includes.size(), 1u); + EXPECT_EQ(result.includes[0].path, "stdlib.h"); +} + +// Module declaration NOT inside conditional (after a closed conditional block). +TEST_CASE(ModuleAfterClosedConditional) { + auto result = scan(R"( +module; +#ifdef FOO +#include +#endif +export module mylib; +)"); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); + EXPECT_FALSE(result.need_preprocess); +} + +// Private module fragment marker should not override the real module declaration. +TEST_CASE(PrivateModuleFragment) { + auto result = scan(R"( +export module mylib; +export int f(); +module : private; +int f() { return 42; } +)"); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); +} + +}; // TEST_SUITE(ModuleScan) + +// ============================================================================= +// scan_module_decl() — lightweight preprocessor fallback +// ============================================================================= + +TEST_SUITE(ModuleDeclFallback) { + +TEST_CASE(Basic) { + ModuleScanFixture f("main.cppm", "export module mylib;"); + auto result = f.decl(); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); +} + +TEST_CASE(ConditionalWithDefine) { + // Without -DUSE_MODULES: no module declaration. + ModuleScanFixture f1("main.cppm", R"( +#ifdef USE_MODULES +export module mylib; +#endif +)"); + EXPECT_TRUE(f1.decl().module_name.empty()); + + // With -DUSE_MODULES: module declaration found. + ModuleScanFixture f2("main.cppm", + R"( +#ifdef USE_MODULES +export module mylib; +#endif +)", + {"-DUSE_MODULES"}); + auto result = f2.decl(); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); +} + +TEST_CASE(ConditionalIfExpr) { + // Without the define: no module. + ModuleScanFixture f1("main.cppm", R"( +#if ENABLE_MODULES >= 1 +export module mylib; +#endif +)"); + EXPECT_TRUE(f1.decl().module_name.empty()); + + // With the define: module found. + ModuleScanFixture f2("main.cppm", + R"( +#if ENABLE_MODULES >= 1 +export module mylib; +#endif +)", + {"-DENABLE_MODULES=1"}); + auto result = f2.decl(); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); +} + +TEST_CASE(GMFWithConditional) { + ModuleScanFixture f("main.cppm", R"( +module; +#include "config.h" +#ifdef USE_MODULES +export module mylib; +#endif +)"); + f.add_file("config.h", "#define USE_MODULES 1\n"); + auto result = f.decl(); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_TRUE(result.is_interface_unit); +} + +TEST_CASE(ImplementationUnit) { + ModuleScanFixture f("main.cpp", "module mylib;"); + auto result = f.decl(); + EXPECT_EQ(result.module_name, "mylib"); + EXPECT_FALSE(result.is_interface_unit); +} + +TEST_CASE(DottedName) { + ModuleScanFixture f("main.cppm", "export module std.io;"); + auto result = f.decl(); + EXPECT_EQ(result.module_name, "std.io"); + EXPECT_TRUE(result.is_interface_unit); +} + +TEST_CASE(Partition) { + ModuleScanFixture f("main.cppm", "export module mylib:core;"); + auto result = f.decl(); + EXPECT_EQ(result.module_name, "mylib:core"); + EXPECT_TRUE(result.is_interface_unit); +} + +TEST_CASE(NoModule) { + ModuleScanFixture f("main.cpp", "int main() { return 0; }"); + auto result = f.decl(); + EXPECT_TRUE(result.module_name.empty()); + EXPECT_FALSE(result.is_interface_unit); + EXPECT_TRUE(result.modules.empty()); +} + +}; // TEST_SUITE(ModuleDeclFallback) + +} // namespace +} // namespace clice::testing