From 981dbe1bde271712c5cf155eba04cd9232991cf8 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 7 Apr 2026 02:33:09 +0900 Subject: [PATCH 1/6] ci(github): rename human output workflow to regression check --- .github/workflows/human-output-regression.yml | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/human-output-regression.yml diff --git a/.github/workflows/human-output-regression.yml b/.github/workflows/human-output-regression.yml new file mode 100644 index 0000000..a4db18c --- /dev/null +++ b/.github/workflows/human-output-regression.yml @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: Apache-2.0 +name: human-output-regression + +on: + push: + branches: [main, master] + pull_request: + +permissions: + contents: read + +jobs: + pytest-human-output: + name: pytest human output (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + runner: [ubuntu-24.04, macos-14] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: .github/workflows/human-output-regression.yml + + - name: Install pytest + run: python3 -m pip install --upgrade pip pytest + + - name: Install LLVM 20 and build tools on Ubuntu + if: runner.os == 'Linux' + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 20 + sudo apt-get update + sudo apt-get install -y clang-20 llvm-20-dev libclang-20-dev cmake ninja-build + + - name: Install LLVM 20 and build tools on macOS + if: runner.os == 'macOS' + run: brew install llvm@20 cmake ninja + + - name: Configure toolchain environment + shell: bash + run: | + if [[ "${RUNNER_OS}" == "Linux" ]]; then + llvm_prefix="/usr/lib/llvm-20" + clang_executable="/usr/bin/clang-20" + else + llvm_prefix="$(brew --prefix llvm@20)" + clang_executable="${llvm_prefix}/bin/clang" + fi + + echo "${llvm_prefix}/bin" >> "${GITHUB_PATH}" + echo "LLVM_DIR=${llvm_prefix}/lib/cmake/llvm" >> "${GITHUB_ENV}" + echo "Clang_DIR=${llvm_prefix}/lib/cmake/clang" >> "${GITHUB_ENV}" + echo "CLANG_EXECUTABLE=${clang_executable}" >> "${GITHUB_ENV}" + echo "CLANG_RESOURCE_DIR=$("${clang_executable}" -print-resource-dir)" >> "${GITHUB_ENV}" + + - name: Configure + run: | + cmake -S . -B build -G Ninja \ + -DLLVM_DIR="${LLVM_DIR}" \ + -DClang_DIR="${Clang_DIR}" \ + -DCLANG_EXECUTABLE="${CLANG_EXECUTABLE}" \ + -DCLANG_RESOURCE_DIR="${CLANG_RESOURCE_DIR}" + + - name: Build analyzer binary + run: cmake --build build --target coretrace_concurrency_analyzer --parallel + + - name: Run human-output golden tests + env: + CORETRACE_ANALYZER_BIN: ./build/coretrace_concurrency_analyzer + run: python3 -m pytest tests/integration/cli/test_human_output_golden.py -q From 33e01fa860787541adfd2b17225957183438c183 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 7 Apr 2026 02:39:19 +0900 Subject: [PATCH 2/6] feat(analyzer): add single-tu concurrency analysis --- CMakeLists.txt | 19 + include/coretrace_concurrency_analysis.hpp | 196 +++++++ main.cpp | 181 ++++++- src/coretrace_concurrency_analysis.cpp | 17 + .../concurrency_symbol_classifier.cpp | 91 ++++ .../concurrency_symbol_classifier.hpp | 33 ++ src/internal/analysis/data_race_checker.cpp | 463 +++++++++++++++++ src/internal/analysis/data_race_checker.hpp | 19 + src/internal/analysis/facts.hpp | 59 +++ src/internal/analysis/ir_utils.cpp | 95 ++++ src/internal/analysis/ir_utils.hpp | 24 + src/internal/analysis/lock_scope_tracker.cpp | 170 ++++++ src/internal/analysis/lock_scope_tracker.hpp | 30 ++ .../analysis/shared_access_collector.cpp | 73 +++ .../analysis/shared_access_collector.hpp | 20 + .../analysis/thread_spawn_detector.cpp | 97 ++++ .../analysis/thread_spawn_detector.hpp | 34 ++ src/internal/analysis/tu_facts_builder.cpp | 64 +++ src/internal/analysis/tu_facts_builder.hpp | 18 + src/internal/compile_command_builder.cpp | 1 + .../compiler_diagnostic_parser.cpp | 279 ++++++++++ .../compiler_diagnostic_parser.hpp | 14 + .../diagnostics/diagnostic_builder.hpp | 95 ++++ .../diagnostics/diagnostic_catalog.cpp | 43 ++ .../diagnostics/diagnostic_catalog.hpp | 28 + src/internal/reporting/report_renderer.cpp | 486 ++++++++++++++++++ src/internal/reporting/report_renderer.hpp | 23 + 27 files changed, 2663 insertions(+), 9 deletions(-) create mode 100644 include/coretrace_concurrency_analysis.hpp create mode 100644 src/coretrace_concurrency_analysis.cpp create mode 100644 src/internal/analysis/concurrency_symbol_classifier.cpp create mode 100644 src/internal/analysis/concurrency_symbol_classifier.hpp create mode 100644 src/internal/analysis/data_race_checker.cpp create mode 100644 src/internal/analysis/data_race_checker.hpp create mode 100644 src/internal/analysis/facts.hpp create mode 100644 src/internal/analysis/ir_utils.cpp create mode 100644 src/internal/analysis/ir_utils.hpp create mode 100644 src/internal/analysis/lock_scope_tracker.cpp create mode 100644 src/internal/analysis/lock_scope_tracker.hpp create mode 100644 src/internal/analysis/shared_access_collector.cpp create mode 100644 src/internal/analysis/shared_access_collector.hpp create mode 100644 src/internal/analysis/thread_spawn_detector.cpp create mode 100644 src/internal/analysis/thread_spawn_detector.hpp create mode 100644 src/internal/analysis/tu_facts_builder.cpp create mode 100644 src/internal/analysis/tu_facts_builder.hpp create mode 100644 src/internal/diagnostics/compiler_diagnostic_parser.cpp create mode 100644 src/internal/diagnostics/compiler_diagnostic_parser.hpp create mode 100644 src/internal/diagnostics/diagnostic_builder.hpp create mode 100644 src/internal/diagnostics/diagnostic_catalog.cpp create mode 100644 src/internal/diagnostics/diagnostic_catalog.hpp create mode 100644 src/internal/reporting/report_renderer.cpp create mode 100644 src/internal/reporting/report_renderer.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 35b5c7a..1a59f65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,18 @@ option(BUILD_CLI "Build coretrace_concurrency_analyzer CLI tool" ON) option(BUILD_SHARED_LIB "Build shared library variant" OFF) set(CONCURRENCY_ANALYZER_SOURCES + src/coretrace_concurrency_analysis.cpp src/coretrace_concurrency_error.cpp + src/internal/analysis/concurrency_symbol_classifier.cpp + src/internal/analysis/data_race_checker.cpp + src/internal/diagnostics/compiler_diagnostic_parser.cpp + src/internal/diagnostics/diagnostic_catalog.cpp + src/internal/analysis/ir_utils.cpp + src/internal/analysis/lock_scope_tracker.cpp + src/internal/reporting/report_renderer.cpp + src/internal/analysis/shared_access_collector.cpp + src/internal/analysis/thread_spawn_detector.cpp + src/internal/analysis/tu_facts_builder.cpp src/internal/compile_command_builder.cpp src/internal/compilation_backend.cpp src/internal/ir_loader.cpp @@ -54,6 +65,8 @@ target_include_directories(coretrace_concurrency_analyzer_lib PUBLIC $ $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src ) target_include_directories(coretrace_concurrency_analyzer_lib SYSTEM @@ -76,6 +89,7 @@ if(LLVM_LINK_LLVM_DYLIB AND TARGET LLVM) target_link_libraries(coretrace_concurrency_analyzer_lib PUBLIC LLVM) else() llvm_map_components_to_libnames(coretrace_concurrency_llvm_libs + analysis core irreader support @@ -94,6 +108,11 @@ if(BUILD_CLI) main.cpp ) + target_include_directories(coretrace_concurrency_analyzer + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + target_link_libraries(coretrace_concurrency_analyzer PRIVATE coretrace_concurrency_analyzer_lib diff --git a/include/coretrace_concurrency_analysis.hpp b/include/coretrace_concurrency_analysis.hpp new file mode 100644 index 0000000..2af234c --- /dev/null +++ b/include/coretrace_concurrency_analysis.hpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace llvm +{ + class Module; +} // namespace llvm + +namespace ctrace::concurrency +{ + enum class AccessKind + { + Read, + Write, + }; + + enum class Severity + { + Info, + Warning, + Error, + }; + + enum class RuleId + { + CompilerDiagnostic, + DataRaceGlobal, + }; + + enum class ConfidenceLevel + { + Low, + Medium, + High, + }; + + enum class OutputFormat + { + Human, + Json, + Sarif, + }; + + struct SourceLocation + { + std::string file; + unsigned line = 0; + unsigned column = 0; + unsigned endLine = 0; + unsigned endColumn = 0; + std::string function; + }; + + struct TaxonomyRef + { + std::string scheme; + std::string id; + std::string title; + }; + + struct RelatedLocation + { + std::string label; + SourceLocation location; + }; + + struct DiagnosticNote + { + std::string text; + }; + + using DiagnosticPropertyValue = + std::variant>; + + struct Diagnostic + { + std::string id; + Severity severity = Severity::Info; + RuleId ruleId = RuleId::DataRaceGlobal; + std::optional confidence; + std::vector taxonomies; + SourceLocation location; + std::vector relatedLocations; + std::string message; + std::vector notes; + std::map properties; + }; + + struct DiagnosticSummary + { + std::size_t info = 0; + std::size_t warning = 0; + std::size_t error = 0; + }; + + struct FunctionSummary + { + std::string file; + std::string name; + bool threadReachable = false; + std::vector threadEntries; + std::size_t sharedAccessCount = 0; + std::size_t protectedAccessCount = 0; + std::size_t writeAccessCount = 0; + bool hasDiagnostics = false; + }; + + struct DiagnosticReport + { + std::vector functions; + std::vector diagnostics; + DiagnosticSummary diagnosticsSummary; + }; + + using AnalysisReport = DiagnosticReport; + + class SingleTUConcurrencyAnalyzer + { + public: + [[nodiscard]] DiagnosticReport analyze(const llvm::Module& module) const; + }; + + constexpr std::string_view toString(AccessKind kind) + { + switch (kind) + { + case AccessKind::Read: + return "read"; + case AccessKind::Write: + return "write"; + } + return "unknown"; + } + + constexpr std::string_view toString(Severity severity) + { + switch (severity) + { + case Severity::Info: + return "info"; + case Severity::Warning: + return "warning"; + case Severity::Error: + return "error"; + } + return "unknown"; + } + + constexpr std::string_view toString(RuleId ruleId) + { + switch (ruleId) + { + case RuleId::CompilerDiagnostic: + return "CompilerDiagnostic"; + case RuleId::DataRaceGlobal: + return "DataRaceGlobal"; + } + return "UnknownRule"; + } + + constexpr std::string_view toString(ConfidenceLevel confidence) + { + switch (confidence) + { + case ConfidenceLevel::Low: + return "low"; + case ConfidenceLevel::Medium: + return "medium"; + case ConfidenceLevel::High: + return "high"; + } + return "unknown"; + } + + constexpr std::string_view toString(OutputFormat format) + { + switch (format) + { + case OutputFormat::Human: + return "human"; + case OutputFormat::Json: + return "json"; + case OutputFormat::Sarif: + return "sarif"; + } + return "unknown"; + } +} // namespace ctrace::concurrency diff --git a/main.cpp b/main.cpp index d683872..8b73b28 100644 --- a/main.cpp +++ b/main.cpp @@ -1,11 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 #include "coretrace_concurrency_analyzer.hpp" +#include "coretrace_concurrency_analysis.hpp" +#include "internal/diagnostics/compiler_diagnostic_parser.hpp" +#include "internal/reporting/report_renderer.hpp" #include #include #include +#include #include +#include #include #include @@ -21,24 +26,35 @@ namespace << " --ir-format=ll|bc Compilation output mode (default: bc)\n" << " --compile-arg= Forward a compile argument (repeatable)\n" << " --instrument Enable compilerlib instrumentation mode\n" + << " --analyze Run single-TU data race analysis on the IR module\n" + << " --format=human|json|sarif\n" + << " Diagnostic output format for --analyze (default: " + "human)\n" << " --verbose Print request details for debugging\n" << " -- Forward all following args to compilerlib\n" << " -h, --help Show this help message\n\n" << "Examples:\n" << " coretrace_concurrency_analyzer test.c --ir-format=ll\n" << " coretrace_concurrency_analyzer test.c --ir-format=bc --compile-arg=-Iinclude\n" + << " coretrace_concurrency_analyzer test.c --analyze --format=human\n" + << " coretrace_concurrency_analyzer test.c --analyze --format=sarif\n" << " coretrace_concurrency_analyzer test.c -- --std=gnu11 -Wall\n"; } - void printRequestSummary(const ctrace::concurrency::CompileRequest& request) + void printRequestSummary(const ctrace::concurrency::CompileRequest& request, bool analyze, + ctrace::concurrency::OutputFormat outputFormat, + llvm::raw_ostream& stream) { - llvm::outs() << "request.input-file: " << request.inputFile << "\n"; - llvm::outs() << "request.ir-format: " << ctrace::concurrency::toString(request.format) - << "\n"; - llvm::outs() << "request.instrument: " << (request.instrument ? "true" : "false") << "\n"; - llvm::outs() << "request.extra-args-count: " << request.extraCompileArgs.size() << "\n"; + stream << "request.input-file: " << request.inputFile << "\n"; + stream << "request.ir-format: " << ctrace::concurrency::toString(request.format) << "\n"; + stream << "request.instrument: " << (request.instrument ? "true" : "false") << "\n"; + stream << "request.analyze: " << (analyze ? "true" : "false") << "\n"; + if (analyze) + stream << "request.output-format: " << ctrace::concurrency::toString(outputFormat) + << "\n"; + stream << "request.extra-args-count: " << request.extraCompileArgs.size() << "\n"; for (const std::string& arg : request.extraCompileArgs) - llvm::outs() << "request.extra-arg: " << arg << "\n"; + stream << "request.extra-arg: " << arg << "\n"; } std::size_t countDefinedFunctions(const llvm::Module& module) @@ -66,13 +82,109 @@ namespace } return false; } + + bool parseOutputFormat(std::string_view value, ctrace::concurrency::OutputFormat& out) + { + if (value == "human") + { + out = ctrace::concurrency::OutputFormat::Human; + return true; + } + + if (value == "json") + { + out = ctrace::concurrency::OutputFormat::Json; + return true; + } + + if (value == "sarif") + { + out = ctrace::concurrency::OutputFormat::Sarif; + return true; + } + + return false; + } + + ctrace::concurrency::internal::reporting::RenderContext + makeRenderContext(std::string_view inputFile, std::int64_t analysisTimeMs = -1) + { + std::error_code currentPathError; + const std::filesystem::path sourceRoot = std::filesystem::current_path(currentPathError); + + return ctrace::concurrency::internal::reporting::RenderContext{ + .toolName = "coretrace-concurrency-analyzer", + .inputFile = std::string(inputFile), + .mode = "IR", + .analysisTimeMs = analysisTimeMs, + .sourceRoot = currentPathError ? std::filesystem::path{} : sourceRoot, + }; + } + + void + emitStructuredReport(const ctrace::concurrency::DiagnosticReport& report, + const ctrace::concurrency::internal::reporting::RenderContext& context, + ctrace::concurrency::OutputFormat outputFormat) + { + std::string rendered = + ctrace::concurrency::internal::reporting::renderReport(report, context, outputFormat); + llvm::outs() << rendered; + if (!rendered.empty() && rendered.back() != '\n') + llvm::outs() << "\n"; + } + + bool hasStructuredLocations(const ctrace::concurrency::DiagnosticReport& report) + { + for (const ctrace::concurrency::Diagnostic& diagnostic : report.diagnostics) + { + if (diagnostic.location.line != 0 || diagnostic.location.column != 0) + return true; + } + return false; + } + + ctrace::concurrency::DiagnosticReport + buildCompileFailureReport(const ctrace::concurrency::CompileRequest& request, + const ctrace::concurrency::CompileResult& result, + ctrace::concurrency::InMemoryIRCompiler& compiler) + { + ctrace::concurrency::DiagnosticReport report = + ctrace::concurrency::internal::diagnostics::parseCompilerDiagnostics( + result.diagnostics, result.error, request.inputFile); + + if (request.format != ctrace::concurrency::IRFormat::BC || hasStructuredLocations(report)) + return report; + + ctrace::concurrency::CompileRequest llRequest = request; + llRequest.format = ctrace::concurrency::IRFormat::LL; + + llvm::LLVMContext retryContext; + const ctrace::concurrency::CompileResult llResult = + compiler.compile(llRequest, retryContext); + if (llResult.success) + return report; + + const ctrace::concurrency::DiagnosticReport recovered = + ctrace::concurrency::internal::diagnostics::parseCompilerDiagnostics( + llResult.diagnostics, llResult.error, request.inputFile); + if (hasStructuredLocations(recovered)) + return recovered; + + if (!llResult.diagnostics.empty()) + return recovered; + + return report; + } } // namespace int main(int argc, char** argv) { ctrace::concurrency::CompileRequest request; + bool analyze = false; bool passthroughMode = false; bool verbose = false; + bool outputFormatExplicit = false; + ctrace::concurrency::OutputFormat outputFormat = ctrace::concurrency::OutputFormat::Human; for (int i = 1; i < argc; ++i) { @@ -102,6 +214,12 @@ int main(int argc, char** argv) continue; } + if (arg == "--analyze") + { + analyze = true; + continue; + } + constexpr std::string_view formatPrefix = "--ir-format="; if (arg.rfind(formatPrefix, 0) == 0) { @@ -113,6 +231,18 @@ int main(int argc, char** argv) continue; } + constexpr std::string_view outputFormatPrefix = "--format="; + if (arg.rfind(outputFormatPrefix, 0) == 0) + { + if (!parseOutputFormat(arg.substr(outputFormatPrefix.size()), outputFormat)) + { + llvm::errs() << "Unsupported --format value: " << std::string(arg) << "\n"; + return 1; + } + outputFormatExplicit = true; + continue; + } + constexpr std::string_view compileArgPrefix = "--compile-arg="; if (arg.rfind(compileArgPrefix, 0) == 0) { @@ -148,18 +278,37 @@ int main(int argc, char** argv) return 1; } + if (outputFormatExplicit && !analyze) + { + llvm::errs() << "--format requires --analyze\n"; + return 1; + } + if (verbose) - printRequestSummary(request); + { + llvm::raw_ostream& stream = + (analyze && outputFormat != ctrace::concurrency::OutputFormat::Human) ? llvm::errs() + : llvm::outs(); + printRequestSummary(request, analyze, outputFormat, stream); + } llvm::LLVMContext context; ctrace::concurrency::InMemoryIRCompiler compiler; ctrace::concurrency::CompileResult result = compiler.compile(request, context); - if (!result.diagnostics.empty()) + if (!analyze && !result.diagnostics.empty()) llvm::errs() << result.diagnostics; if (!result.success) { + if (analyze) + { + const ctrace::concurrency::DiagnosticReport report = + buildCompileFailureReport(request, result, compiler); + emitStructuredReport(report, makeRenderContext(request.inputFile), outputFormat); + return 1; + } + const std::string renderedError = formatCompileError(result.error); if (!renderedError.empty()) llvm::errs() << renderedError << "\n"; @@ -168,6 +317,20 @@ int main(int argc, char** argv) return 1; } + if (analyze) + { + const auto startedAt = std::chrono::steady_clock::now(); + ctrace::concurrency::SingleTUConcurrencyAnalyzer analyzer; + const ctrace::concurrency::DiagnosticReport report = analyzer.analyze(*result.module); + const auto finishedAt = std::chrono::steady_clock::now(); + + const auto duration = + std::chrono::duration_cast(finishedAt - startedAt).count(); + + emitStructuredReport(report, makeRenderContext(request.inputFile, duration), outputFormat); + return 0; + } + const std::size_t payloadBytes = (request.format == ctrace::concurrency::IRFormat::BC) ? result.llvmBitcode.size() : result.llvmIRText.size(); diff --git a/src/coretrace_concurrency_analysis.cpp b/src/coretrace_concurrency_analysis.cpp new file mode 100644 index 0000000..a335507 --- /dev/null +++ b/src/coretrace_concurrency_analysis.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "coretrace_concurrency_analysis.hpp" + +#include "internal/analysis/data_race_checker.hpp" +#include "internal/analysis/tu_facts_builder.hpp" + +namespace ctrace::concurrency +{ + DiagnosticReport SingleTUConcurrencyAnalyzer::analyze(const llvm::Module& module) const + { + internal::analysis::TUFactsBuilder factsBuilder; + const internal::analysis::TUFacts facts = factsBuilder.build(module); + + internal::analysis::DataRaceChecker checker; + return checker.run(module, facts); + } +} // namespace ctrace::concurrency diff --git a/src/internal/analysis/concurrency_symbol_classifier.cpp b/src/internal/analysis/concurrency_symbol_classifier.cpp new file mode 100644 index 0000000..0af963a --- /dev/null +++ b/src/internal/analysis/concurrency_symbol_classifier.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "concurrency_symbol_classifier.hpp" + +#include +#include +#include + +namespace ctrace::concurrency::internal::analysis +{ + namespace + { + llvm::StringRef canonicalName(const llvm::Function& function) + { + llvm::StringRef name = function.getName(); + if (name.starts_with("\x01")) + name = name.drop_front(); + return name; + } + + bool isStdThreadCtor(llvm::StringRef name) + { + return name.contains("thread") && + (name.contains("threadC1") || name.contains("threadC2")); + } + + bool isStdMutexLock(llvm::StringRef name) + { + return name.contains("mutex") && name.contains("4lockEv"); + } + + bool isStdMutexUnlock(llvm::StringRef name) + { + return name.contains("mutex") && name.contains("6unlockEv"); + } + } // namespace + + const llvm::Function* + ConcurrencySymbolClassifier::directCallee(const llvm::CallBase& call) const + { + const llvm::Value* calledOperand = call.getCalledOperand(); + if (calledOperand == nullptr) + return nullptr; + + calledOperand = calledOperand->stripPointerCasts(); + return llvm::dyn_cast(calledOperand); + } + + CallKind ConcurrencySymbolClassifier::classify(const llvm::CallBase& call) const + { + const llvm::Function* callee = directCallee(call); + if (callee == nullptr) + return CallKind::Unknown; + + const llvm::StringRef name = canonicalName(*callee); + if (name == "pthread_create") + return CallKind::PThreadCreate; + if (name == "pthread_mutex_lock") + return CallKind::PThreadMutexLock; + if (name == "pthread_mutex_unlock") + return CallKind::PThreadMutexUnlock; + if (isStdThreadCtor(name)) + return CallKind::StdThreadCtor; + if (isStdMutexLock(name)) + return CallKind::StdMutexLock; + if (isStdMutexUnlock(name)) + return CallKind::StdMutexUnlock; + return CallKind::Unknown; + } + + std::string_view ConcurrencySymbolClassifier::toString(CallKind kind) + { + switch (kind) + { + case CallKind::Unknown: + return "unknown"; + case CallKind::PThreadCreate: + return "pthread_create"; + case CallKind::PThreadMutexLock: + return "pthread_mutex_lock"; + case CallKind::PThreadMutexUnlock: + return "pthread_mutex_unlock"; + case CallKind::StdThreadCtor: + return "std_thread_ctor"; + case CallKind::StdMutexLock: + return "std_mutex_lock"; + case CallKind::StdMutexUnlock: + return "std_mutex_unlock"; + } + return "unknown"; + } +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/concurrency_symbol_classifier.hpp b/src/internal/analysis/concurrency_symbol_classifier.hpp new file mode 100644 index 0000000..ca6c8f3 --- /dev/null +++ b/src/internal/analysis/concurrency_symbol_classifier.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +namespace llvm +{ + class CallBase; + class Function; +} // namespace llvm + +namespace ctrace::concurrency::internal::analysis +{ + enum class CallKind + { + Unknown, + PThreadCreate, + PThreadMutexLock, + PThreadMutexUnlock, + StdThreadCtor, + StdMutexLock, + StdMutexUnlock, + }; + + class ConcurrencySymbolClassifier + { + public: + [[nodiscard]] const llvm::Function* directCallee(const llvm::CallBase& call) const; + [[nodiscard]] CallKind classify(const llvm::CallBase& call) const; + [[nodiscard]] static std::string_view toString(CallKind kind); + }; +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/data_race_checker.cpp b/src/internal/analysis/data_race_checker.cpp new file mode 100644 index 0000000..b562431 --- /dev/null +++ b/src/internal/analysis/data_race_checker.cpp @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "data_race_checker.hpp" + +#include "internal/diagnostics/diagnostic_builder.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ctrace::concurrency::internal::analysis +{ + namespace + { + using internal::diagnostics::DiagnosticBuilder; + using EntrySet = std::unordered_set; + + std::unordered_map + computeThreadReachability(const llvm::Module& module, const TUFacts& facts) + { + llvm::Module& mutableModule = const_cast(module); + llvm::CallGraph callGraph(mutableModule); + + std::unordered_map reachableEntriesByFunction; + for (const auto& [entryFunctionId, concurrency] : facts.entryConcurrency) + { + (void)concurrency; + llvm::Function* entryFunction = mutableModule.getFunction(entryFunctionId); + if (entryFunction == nullptr || entryFunction->isDeclaration()) + continue; + + std::deque queue; + std::unordered_set visited; + queue.push_back(entryFunction); + visited.insert(entryFunctionId); + + while (!queue.empty()) + { + const llvm::Function* function = queue.front(); + queue.pop_front(); + reachableEntriesByFunction[function->getName().str()].insert(entryFunctionId); + + const llvm::CallGraphNode* node = callGraph[function]; + for (const auto& callRecord : *node) + { + if (!callRecord.first.has_value()) + continue; + + const llvm::CallGraphNode* calleeNode = callRecord.second; + if (calleeNode == nullptr) + continue; + + const llvm::Function* callee = calleeNode->getFunction(); + if (callee == nullptr || callee->isDeclaration()) + continue; + + const std::string calleeId = callee->getName().str(); + if (visited.insert(calleeId).second) + queue.push_back(callee); + } + } + } + + return reachableEntriesByFunction; + } + + bool shareRecognizedLock(const AccessFact& lhs, const AccessFact& rhs) + { + if (lhs.heldLocks.empty() || rhs.heldLocks.empty()) + return false; + + return std::any_of(lhs.heldLocks.begin(), lhs.heldLocks.end(), + [&](const std::string& lock) + { return rhs.heldLocks.contains(lock); }); + } + + bool isSelfConcurrent(const EntrySet& entries, const TUFacts& facts) + { + return std::any_of(entries.begin(), entries.end(), + [&](const std::string& entry) + { + const auto it = facts.entryConcurrency.find(entry); + return it != facts.entryConcurrency.end() && + it->second.isSelfConcurrent(); + }); + } + + bool mayRunConcurrently(const EntrySet& lhsEntries, const EntrySet& rhsEntries, + const TUFacts& facts) + { + for (const std::string& lhsEntry : lhsEntries) + { + for (const std::string& rhsEntry : rhsEntries) + { + if (lhsEntry != rhsEntry) + return true; + + const auto it = facts.entryConcurrency.find(lhsEntry); + if (it != facts.entryConcurrency.end() && it->second.isSelfConcurrent()) + return true; + } + } + + return false; + } + + std::vector sortedEntries(const EntrySet& entries) + { + std::vector ordered(entries.begin(), entries.end()); + std::sort(ordered.begin(), ordered.end()); + return ordered; + } + + std::vector sortedLocks(const AccessFact& access) + { + return std::vector(access.heldLocks.begin(), access.heldLocks.end()); + } + + std::string joinValues(const std::vector& values) + { + std::ostringstream stream; + for (std::size_t index = 0; index < values.size(); ++index) + { + if (index != 0) + stream << ", "; + stream << values[index]; + } + return stream.str(); + } + + std::string formatLocation(const SourceLocation& location) + { + std::ostringstream stream; + if (!location.file.empty()) + stream << location.file; + else + stream << ""; + + if (location.line != 0) + stream << ":" << location.line << ":" << location.column; + + if (!location.function.empty()) + stream << " in " << location.function; + return stream.str(); + } + + bool sameSourceLocation(const SourceLocation& lhs, const SourceLocation& rhs) + { + return std::tie(lhs.file, lhs.line, lhs.column, lhs.function) == + std::tie(rhs.file, rhs.line, rhs.column, rhs.function); + } + + bool shareSelfConcurrentEntry(const EntrySet& lhsEntries, const EntrySet& rhsEntries, + const TUFacts& facts) + { + for (const std::string& lhsEntry : lhsEntries) + { + if (!rhsEntries.contains(lhsEntry)) + continue; + + const auto it = facts.entryConcurrency.find(lhsEntry); + if (it != facts.entryConcurrency.end() && it->second.isSelfConcurrent()) + return true; + } + + return false; + } + + std::string conflictKindLabel(AccessKind lhsKind, AccessKind rhsKind) + { + if (lhsKind == AccessKind::Write && rhsKind == AccessKind::Write) + return "write/write"; + + if (lhsKind == AccessKind::Read && rhsKind == AccessKind::Read) + return "read/read"; + + return "read/write"; + } + + std::vector collectConflictKinds(const AccessFact& lhs, const AccessFact& rhs, + const EntrySet& lhsEntries, + const EntrySet& rhsEntries, + const TUFacts& facts) + { + std::set conflictKinds; + conflictKinds.insert(conflictKindLabel(lhs.kind, rhs.kind)); + + if (sameSourceLocation(lhs.location, rhs.location) && + shareSelfConcurrentEntry(lhsEntries, rhsEntries, facts) && + (lhs.kind == AccessKind::Write || rhs.kind == AccessKind::Write)) + { + conflictKinds.insert("write/write"); + } + + return std::vector(conflictKinds.begin(), conflictKinds.end()); + } + + std::string describeAccess(const AccessFact& access, + const std::vector& entries) + { + std::ostringstream stream; + stream << toString(access.kind) << " at " << formatLocation(access.location); + + if (!entries.empty()) + stream << " (thread entries: " << joinValues(entries) << ")"; + + if (!access.heldLocks.empty()) + stream << " under recognized lock(s): " << joinValues(sortedLocks(access)); + + return stream.str(); + } + + void emitPairDiagnostic(DiagnosticReport& report, const AccessFact& lhs, + const AccessFact& rhs, const EntrySet& lhsEntries, + const EntrySet& rhsEntries, const TUFacts& facts) + { + const std::vector orderedLhsEntries = sortedEntries(lhsEntries); + const std::vector orderedRhsEntries = sortedEntries(rhsEntries); + const std::vector conflictKinds = + collectConflictKinds(lhs, rhs, lhsEntries, rhsEntries, facts); + + DiagnosticBuilder(report, RuleId::DataRaceGlobal) + .primaryLocation(lhs.location) + .relatedLocation("Conflicting access", rhs.location) + .message("unsynchronized concurrent access to global '" + lhs.symbol + "'") + .note("first access: " + describeAccess(lhs, orderedLhsEntries)) + .note("conflicting access: " + describeAccess(rhs, orderedRhsEntries)) + .note("possible conflict kinds: " + joinValues(conflictKinds)) + .note("no common recognized lock protects the conflicting accesses") + .property("symbol", lhs.symbol) + .property("firstAccessKind", std::string(toString(lhs.kind))) + .property("secondAccessKind", std::string(toString(rhs.kind))) + .property("firstProtected", !lhs.heldLocks.empty()) + .property("secondProtected", !rhs.heldLocks.empty()) + .property("firstThreadEntries", orderedLhsEntries) + .property("secondThreadEntries", orderedRhsEntries) + .property("conflictKinds", conflictKinds) + .property("variableAliasing", std::vector{}) + .emit(); + } + + void emitSelfConcurrentDiagnostic(DiagnosticReport& report, const AccessFact& access, + const EntrySet& entries) + { + const std::vector orderedEntries = sortedEntries(entries); + const std::string entryLabel = + orderedEntries.empty() ? access.functionId : joinValues(orderedEntries); + const std::vector conflictKinds = {"write/write"}; + + DiagnosticBuilder(report, RuleId::DataRaceGlobal) + .primaryLocation(access.location) + .relatedLocation("Concurrent invocation", access.location) + .message("unsynchronized concurrent access to global '" + access.symbol + "'") + .note("access: " + describeAccess(access, orderedEntries)) + .note("conflicts with another concurrent invocation reachable from thread entry " + "'" + + entryLabel + "'") + .note("possible conflict kinds: " + joinValues(conflictKinds)) + .note("no common recognized lock protects the conflicting accesses") + .property("symbol", access.symbol) + .property("firstAccessKind", std::string(toString(access.kind))) + .property("secondAccessKind", std::string(toString(access.kind))) + .property("firstProtected", !access.heldLocks.empty()) + .property("secondProtected", !access.heldLocks.empty()) + .property("firstThreadEntries", orderedEntries) + .property("secondThreadEntries", orderedEntries) + .property("conflictKinds", conflictKinds) + .property("variableAliasing", std::vector{}) + .emit(); + } + + DiagnosticSummary computeSummary(const std::vector& diagnostics) + { + DiagnosticSummary summary; + for (const Diagnostic& diagnostic : diagnostics) + { + switch (diagnostic.severity) + { + case Severity::Info: + ++summary.info; + break; + case Severity::Warning: + ++summary.warning; + break; + case Severity::Error: + ++summary.error; + break; + } + } + return summary; + } + + std::vector + buildFunctionSummaries(const TUFacts& facts, + const std::unordered_map& reachableEntries, + const std::vector& diagnostics) + { + std::map functions; + + auto ensureSummary = [&](const std::string& functionId) -> FunctionSummary& + { + FunctionSummary& summary = functions[functionId]; + if (summary.name.empty()) + summary.name = functionId; + return summary; + }; + + for (const AccessFact& access : facts.accesses) + { + FunctionSummary& summary = ensureSummary(access.functionId); + if (summary.name.empty() && !access.location.function.empty()) + summary.name = access.location.function; + else if (!access.location.function.empty()) + summary.name = access.location.function; + + if (summary.file.empty() && !access.location.file.empty()) + summary.file = access.location.file; + + ++summary.sharedAccessCount; + if (!access.heldLocks.empty()) + ++summary.protectedAccessCount; + if (access.kind == AccessKind::Write) + ++summary.writeAccessCount; + } + + for (const auto& [functionId, entries] : reachableEntries) + { + FunctionSummary& summary = ensureSummary(functionId); + summary.threadReachable = true; + summary.threadEntries = sortedEntries(entries); + } + + for (const Diagnostic& diagnostic : diagnostics) + { + const std::string functionName = diagnostic.location.function.empty() + ? std::string{} + : diagnostic.location.function; + if (!functionName.empty()) + { + for (auto& [_, summary] : functions) + { + if (summary.name == functionName) + summary.hasDiagnostics = true; + } + } + + for (const RelatedLocation& related : diagnostic.relatedLocations) + { + if (related.location.function.empty()) + continue; + + for (auto& [_, summary] : functions) + { + if (summary.name == related.location.function) + summary.hasDiagnostics = true; + } + } + } + + std::vector ordered; + ordered.reserve(functions.size()); + for (auto& [_, summary] : functions) + ordered.push_back(std::move(summary)); + + std::sort(ordered.begin(), ordered.end(), + [](const FunctionSummary& lhs, const FunctionSummary& rhs) + { return std::tie(lhs.name, lhs.file) < std::tie(rhs.name, rhs.file); }); + return ordered; + } + + void finalizeReport(DiagnosticReport& report) + { + std::sort(report.diagnostics.begin(), report.diagnostics.end(), + [](const Diagnostic& lhs, const Diagnostic& rhs) + { + return std::tie(lhs.ruleId, lhs.location.file, lhs.location.line, + lhs.location.column, lhs.message) < + std::tie(rhs.ruleId, rhs.location.file, rhs.location.line, + rhs.location.column, rhs.message); + }); + + for (std::size_t index = 0; index < report.diagnostics.size(); ++index) + report.diagnostics[index].id = "diag-" + std::to_string(index + 1); + + report.diagnosticsSummary = computeSummary(report.diagnostics); + } + } // namespace + + DiagnosticReport DataRaceChecker::run(const llvm::Module& module, const TUFacts& facts) const + { + const std::unordered_map reachableEntriesByFunction = + computeThreadReachability(module, facts); + + std::map> accessesBySymbol; + for (const AccessFact& access : facts.accesses) + { + if (!reachableEntriesByFunction.contains(access.functionId)) + continue; + accessesBySymbol[access.symbol].push_back(&access); + } + + DiagnosticReport report; + for (const auto& [symbol, accesses] : accessesBySymbol) + { + (void)symbol; + bool foundPairDiagnostic = false; + for (std::size_t lhsIndex = 0; lhsIndex < accesses.size(); ++lhsIndex) + { + for (std::size_t rhsIndex = lhsIndex + 1; rhsIndex < accesses.size(); ++rhsIndex) + { + const AccessFact& lhs = *accesses[lhsIndex]; + const AccessFact& rhs = *accesses[rhsIndex]; + + if (lhs.kind != AccessKind::Write && rhs.kind != AccessKind::Write) + continue; + + const EntrySet& lhsEntries = reachableEntriesByFunction.at(lhs.functionId); + const EntrySet& rhsEntries = reachableEntriesByFunction.at(rhs.functionId); + if (!mayRunConcurrently(lhsEntries, rhsEntries, facts)) + continue; + + if (shareRecognizedLock(lhs, rhs)) + continue; + + emitPairDiagnostic(report, lhs, rhs, lhsEntries, rhsEntries, facts); + foundPairDiagnostic = true; + } + } + + if (foundPairDiagnostic) + continue; + + for (const AccessFact* access : accesses) + { + if (access->kind != AccessKind::Write) + continue; + + const EntrySet& entries = reachableEntriesByFunction.at(access->functionId); + if (!isSelfConcurrent(entries, facts)) + continue; + + if (shareRecognizedLock(*access, *access)) + continue; + + emitSelfConcurrentDiagnostic(report, *access, entries); + break; + } + } + + report.functions = + buildFunctionSummaries(facts, reachableEntriesByFunction, report.diagnostics); + finalizeReport(report); + return report; + } +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/data_race_checker.hpp b/src/internal/analysis/data_race_checker.hpp new file mode 100644 index 0000000..a6cdb92 --- /dev/null +++ b/src/internal/analysis/data_race_checker.hpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "coretrace_concurrency_analysis.hpp" +#include "facts.hpp" + +namespace llvm +{ + class Module; +} // namespace llvm + +namespace ctrace::concurrency::internal::analysis +{ + class DataRaceChecker + { + public: + [[nodiscard]] DiagnosticReport run(const llvm::Module& module, const TUFacts& facts) const; + }; +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/facts.hpp b/src/internal/analysis/facts.hpp new file mode 100644 index 0000000..e4a557a --- /dev/null +++ b/src/internal/analysis/facts.hpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "coretrace_concurrency_analysis.hpp" + +#include +#include +#include +#include + +namespace llvm +{ + class Function; + class Instruction; +} // namespace llvm + +namespace ctrace::concurrency::internal::analysis +{ + struct EntryConcurrencyInfo + { + std::size_t staticSpawnCount = 0; + bool hasSpawnInLoop = false; + + [[nodiscard]] bool isSelfConcurrent() const noexcept + { + return staticSpawnCount >= 2 || hasSpawnInLoop; + } + }; + + struct SpawnFact + { + std::string entryFunctionId; + SourceLocation location; + bool insideLoop = false; + }; + + struct AccessFact + { + std::string symbol; + std::string functionId; + AccessKind kind = AccessKind::Read; + SourceLocation location; + std::set heldLocks; + }; + + struct PendingAccess + { + const llvm::Function* function = nullptr; + const llvm::Instruction* instruction = nullptr; + AccessFact fact; + }; + + struct TUFacts + { + std::vector spawns; + std::vector accesses; + std::unordered_map entryConcurrency; + }; +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/ir_utils.cpp b/src/internal/analysis/ir_utils.cpp new file mode 100644 index 0000000..ebd0414 --- /dev/null +++ b/src/internal/analysis/ir_utils.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "ir_utils.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace ctrace::concurrency::internal::analysis +{ + namespace + { + std::string normalizeFunctionName(llvm::StringRef name) + { + if (name.starts_with("\x01")) + name = name.drop_front(); + return name.str(); + } + } // namespace + + const llvm::GlobalVariable* resolveBaseGlobal(const llvm::Value& value) + { + const llvm::Value* current = value.stripPointerCastsAndAliases(); + while (current != nullptr) + { + if (const auto* global = llvm::dyn_cast(current)) + return global; + + if (const auto* gep = llvm::dyn_cast(current)) + { + current = gep->getPointerOperand()->stripPointerCastsAndAliases(); + continue; + } + + return nullptr; + } + + return nullptr; + } + + std::optional canonicalGlobalId(const llvm::Value& value) + { + const llvm::GlobalVariable* global = resolveBaseGlobal(value); + if (global == nullptr) + return std::nullopt; + + return normalizeFunctionName(global->getName()); + } + + std::string functionId(const llvm::Function& function) + { + return normalizeFunctionName(function.getName()); + } + + std::string functionDisplayName(const llvm::Function& function) + { + if (const llvm::DISubprogram* subprogram = function.getSubprogram()) + { + if (!subprogram->getName().empty()) + return subprogram->getName().str(); + } + + return functionId(function); + } + + SourceLocation makeSourceLocation(const llvm::Instruction& instruction) + { + SourceLocation location; + location.function = functionDisplayName(*instruction.getFunction()); + + const llvm::DebugLoc debugLocation = instruction.getDebugLoc(); + if (!debugLocation) + return location; + + location.line = debugLocation.getLine(); + location.column = debugLocation.getCol(); + location.endLine = location.line; + location.endColumn = location.column; + + const std::string filename = debugLocation->getFilename().str(); + const std::string directory = debugLocation->getDirectory().str(); + if (filename.empty()) + return location; + + std::filesystem::path filePath(filename); + if (!directory.empty() && filePath.is_relative()) + filePath = std::filesystem::path(directory) / filePath; + location.file = filePath.lexically_normal().string(); + return location; + } +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/ir_utils.hpp b/src/internal/analysis/ir_utils.hpp new file mode 100644 index 0000000..59de2a3 --- /dev/null +++ b/src/internal/analysis/ir_utils.hpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "coretrace_concurrency_analysis.hpp" + +#include +#include + +namespace llvm +{ + class Function; + class GlobalVariable; + class Instruction; + class Value; +} // namespace llvm + +namespace ctrace::concurrency::internal::analysis +{ + [[nodiscard]] const llvm::GlobalVariable* resolveBaseGlobal(const llvm::Value& value); + [[nodiscard]] std::optional canonicalGlobalId(const llvm::Value& value); + [[nodiscard]] std::string functionId(const llvm::Function& function); + [[nodiscard]] std::string functionDisplayName(const llvm::Function& function); + [[nodiscard]] SourceLocation makeSourceLocation(const llvm::Instruction& instruction); +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/lock_scope_tracker.cpp b/src/internal/analysis/lock_scope_tracker.cpp new file mode 100644 index 0000000..4ff2d9e --- /dev/null +++ b/src/internal/analysis/lock_scope_tracker.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "lock_scope_tracker.hpp" + +#include "concurrency_symbol_classifier.hpp" +#include "ir_utils.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ctrace::concurrency::internal::analysis +{ + namespace + { + using LockSet = std::set; + using StateMap = std::unordered_map>; + + std::optional> + lockOperation(const llvm::Instruction& instruction, + const ConcurrencySymbolClassifier& classifier) + { + const auto* call = llvm::dyn_cast(&instruction); + if (call == nullptr) + return std::nullopt; + + const CallKind kind = classifier.classify(*call); + const bool isAcquire = + kind == CallKind::PThreadMutexLock || kind == CallKind::StdMutexLock; + const bool isRelease = + kind == CallKind::PThreadMutexUnlock || kind == CallKind::StdMutexUnlock; + if (!isAcquire && !isRelease) + return std::nullopt; + + if (call->arg_size() == 0) + return std::nullopt; + + const std::optional lockId = canonicalGlobalId(*call->getArgOperand(0)); + if (!lockId.has_value()) + return std::nullopt; + + return std::make_pair(isAcquire, *lockId); + } + + LockSet intersectLockSets(const LockSet& lhs, const LockSet& rhs) + { + LockSet result; + std::set_intersection(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), + std::inserter(result, result.end())); + return result; + } + + std::optional meetPredecessorStates(const llvm::BasicBlock& block, + const StateMap& outStates, + const llvm::DominatorTree& dominatorTree) + { + std::optional result; + for (const llvm::BasicBlock* predecessor : llvm::predecessors(&block)) + { + if (!dominatorTree.isReachableFromEntry(predecessor)) + continue; + + const auto it = outStates.find(predecessor); + if (it == outStates.end() || !it->second.has_value()) + continue; + + if (!result.has_value()) + result = *it->second; + else + result = intersectLockSets(*result, *it->second); + } + + if (result.has_value()) + return result; + + if (&block == &block.getParent()->getEntryBlock()) + return LockSet{}; + + return std::nullopt; + } + } // namespace + + LockScopeTracker::LockScopeTracker(const ConcurrencySymbolClassifier& classifier) + : classifier_(classifier) + { + } + + std::unordered_map> + LockScopeTracker::collectHeldLocks( + const llvm::Function& function, + const std::unordered_set& trackedAccesses) const + { + std::unordered_map> heldLocksByAccess; + if (trackedAccesses.empty()) + return heldLocksByAccess; + + llvm::Function& mutableFunction = const_cast(function); + llvm::DominatorTree dominatorTree(mutableFunction); + + std::vector reachableBlocks; + for (const llvm::BasicBlock& block : function) + { + if (dominatorTree.isReachableFromEntry(&block)) + reachableBlocks.push_back(&block); + } + + StateMap inStates; + StateMap outStates; + for (const llvm::BasicBlock* block : reachableBlocks) + { + inStates.emplace(block, std::nullopt); + outStates.emplace(block, std::nullopt); + } + + const llvm::BasicBlock* entryBlock = &function.getEntryBlock(); + inStates[entryBlock] = std::set{}; + + bool changed = true; + while (changed) + { + changed = false; + + for (const llvm::BasicBlock* block : reachableBlocks) + { + std::optional newInState = inStates[block]; + if (block != entryBlock) + newInState = meetPredecessorStates(*block, outStates, dominatorTree); + + if (!newInState.has_value()) + continue; + + LockSet currentLocks = *newInState; + for (const llvm::Instruction& instruction : *block) + { + if (trackedAccesses.contains(&instruction)) + heldLocksByAccess[&instruction] = currentLocks; + + const std::optional> operation = + lockOperation(instruction, classifier_); + if (!operation.has_value()) + continue; + + if (operation->first) + currentLocks.insert(operation->second); + else + currentLocks.erase(operation->second); + } + + if (inStates[block] != newInState) + { + inStates[block] = std::move(newInState); + changed = true; + } + + if (outStates[block] != currentLocks) + { + outStates[block] = std::move(currentLocks); + changed = true; + } + } + } + + return heldLocksByAccess; + } +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/lock_scope_tracker.hpp b/src/internal/analysis/lock_scope_tracker.hpp new file mode 100644 index 0000000..0eb999b --- /dev/null +++ b/src/internal/analysis/lock_scope_tracker.hpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +namespace llvm +{ + class Function; + class Instruction; +} // namespace llvm + +namespace ctrace::concurrency::internal::analysis +{ + class ConcurrencySymbolClassifier; + + class LockScopeTracker + { + public: + explicit LockScopeTracker(const ConcurrencySymbolClassifier& classifier); + + [[nodiscard]] std::unordered_map> + collectHeldLocks(const llvm::Function& function, + const std::unordered_set& trackedAccesses) const; + + private: + const ConcurrencySymbolClassifier& classifier_; + }; +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/shared_access_collector.cpp b/src/internal/analysis/shared_access_collector.cpp new file mode 100644 index 0000000..4458f9c --- /dev/null +++ b/src/internal/analysis/shared_access_collector.cpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "shared_access_collector.hpp" + +#include "ir_utils.hpp" + +#include +#include +#include +#include + +namespace ctrace::concurrency::internal::analysis +{ + namespace + { + bool shouldTrackGlobal(const llvm::GlobalVariable& global) + { + return global.hasExternalLinkage() && !global.isConstant() && !global.isThreadLocal(); + } + } // namespace + + std::vector SharedAccessCollector::collect(const llvm::Module& module) const + { + std::vector accesses; + + for (const llvm::Function& function : module) + { + if (function.isDeclaration()) + continue; + + for (const llvm::BasicBlock& block : function) + { + for (const llvm::Instruction& instruction : block) + { + const llvm::Value* pointerOperand = nullptr; + AccessKind kind = AccessKind::Read; + + if (const auto* load = llvm::dyn_cast(&instruction)) + { + pointerOperand = load->getPointerOperand(); + kind = AccessKind::Read; + } + else if (const auto* store = llvm::dyn_cast(&instruction)) + { + pointerOperand = store->getPointerOperand(); + kind = AccessKind::Write; + } + else + { + continue; + } + + if (pointerOperand == nullptr) + continue; + + const llvm::GlobalVariable* global = resolveBaseGlobal(*pointerOperand); + if (global == nullptr || !shouldTrackGlobal(*global)) + continue; + + PendingAccess access; + access.function = &function; + access.instruction = &instruction; + access.fact.symbol = global->getName().str(); + access.fact.functionId = functionId(function); + access.fact.kind = kind; + access.fact.location = makeSourceLocation(instruction); + accesses.push_back(std::move(access)); + } + } + } + + return accesses; + } +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/shared_access_collector.hpp b/src/internal/analysis/shared_access_collector.hpp new file mode 100644 index 0000000..375d77d --- /dev/null +++ b/src/internal/analysis/shared_access_collector.hpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "facts.hpp" + +#include + +namespace llvm +{ + class Module; +} // namespace llvm + +namespace ctrace::concurrency::internal::analysis +{ + class SharedAccessCollector + { + public: + [[nodiscard]] std::vector collect(const llvm::Module& module) const; + }; +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/thread_spawn_detector.cpp b/src/internal/analysis/thread_spawn_detector.cpp new file mode 100644 index 0000000..77baeae --- /dev/null +++ b/src/internal/analysis/thread_spawn_detector.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "thread_spawn_detector.hpp" + +#include "concurrency_symbol_classifier.hpp" +#include "ir_utils.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ctrace::concurrency::internal::analysis +{ + namespace + { + const llvm::Function* threadEntryFromCall(const llvm::CallBase& call, CallKind kind) + { + const llvm::Value* entryValue = nullptr; + switch (kind) + { + case CallKind::PThreadCreate: + if (call.arg_size() > 2) + entryValue = call.getArgOperand(2); + break; + case CallKind::StdThreadCtor: + if (call.arg_size() > 1) + entryValue = call.getArgOperand(1); + break; + default: + break; + } + + if (entryValue == nullptr) + return nullptr; + + entryValue = entryValue->stripPointerCasts(); + return llvm::dyn_cast(entryValue); + } + } // namespace + + ThreadSpawnDetector::ThreadSpawnDetector(const ConcurrencySymbolClassifier& classifier) + : classifier_(classifier) + { + } + + ThreadSpawnCollection ThreadSpawnDetector::collect(const llvm::Module& module) const + { + ThreadSpawnCollection collection; + + for (const llvm::Function& function : module) + { + if (function.isDeclaration()) + continue; + + llvm::Function& mutableFunction = const_cast(function); + llvm::DominatorTree dominatorTree(mutableFunction); + llvm::LoopInfo loopInfo(dominatorTree); + + for (const llvm::BasicBlock& block : function) + { + if (!dominatorTree.isReachableFromEntry(&block)) + continue; + + for (const llvm::Instruction& instruction : block) + { + const auto* call = llvm::dyn_cast(&instruction); + if (call == nullptr) + continue; + + const CallKind kind = classifier_.classify(*call); + if (kind != CallKind::PThreadCreate && kind != CallKind::StdThreadCtor) + continue; + + const llvm::Function* entry = threadEntryFromCall(*call, kind); + if (entry == nullptr || entry->isDeclaration()) + continue; + + const bool insideLoop = loopInfo.getLoopFor(call->getParent()) != nullptr; + SpawnFact fact; + fact.entryFunctionId = functionId(*entry); + fact.location = makeSourceLocation(instruction); + fact.insideLoop = insideLoop; + collection.spawns.push_back(fact); + + EntryConcurrencyInfo& concurrency = + collection.entryConcurrency[fact.entryFunctionId]; + ++concurrency.staticSpawnCount; + concurrency.hasSpawnInLoop = concurrency.hasSpawnInLoop || insideLoop; + } + } + } + + return collection; + } +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/thread_spawn_detector.hpp b/src/internal/analysis/thread_spawn_detector.hpp new file mode 100644 index 0000000..6daefaa --- /dev/null +++ b/src/internal/analysis/thread_spawn_detector.hpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "facts.hpp" + +#include +#include + +namespace llvm +{ + class Module; +} // namespace llvm + +namespace ctrace::concurrency::internal::analysis +{ + class ConcurrencySymbolClassifier; + + struct ThreadSpawnCollection + { + std::vector spawns; + std::unordered_map entryConcurrency; + }; + + class ThreadSpawnDetector + { + public: + explicit ThreadSpawnDetector(const ConcurrencySymbolClassifier& classifier); + + [[nodiscard]] ThreadSpawnCollection collect(const llvm::Module& module) const; + + private: + const ConcurrencySymbolClassifier& classifier_; + }; +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/tu_facts_builder.cpp b/src/internal/analysis/tu_facts_builder.cpp new file mode 100644 index 0000000..8d329b1 --- /dev/null +++ b/src/internal/analysis/tu_facts_builder.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "tu_facts_builder.hpp" + +#include "concurrency_symbol_classifier.hpp" +#include "lock_scope_tracker.hpp" +#include "shared_access_collector.hpp" +#include "thread_spawn_detector.hpp" + +#include +#include + +#include +#include + +namespace ctrace::concurrency::internal::analysis +{ + TUFacts TUFactsBuilder::build(const llvm::Module& module) const + { + const ConcurrencySymbolClassifier classifier; + + ThreadSpawnDetector spawnDetector(classifier); + ThreadSpawnCollection spawnFacts = spawnDetector.collect(module); + + SharedAccessCollector accessCollector; + std::vector pendingAccesses = accessCollector.collect(module); + + std::unordered_map> + trackedAccessesByFunction; + std::unordered_map functionsById; + for (const PendingAccess& pendingAccess : pendingAccesses) + { + trackedAccessesByFunction[pendingAccess.fact.functionId].insert( + pendingAccess.instruction); + functionsById[pendingAccess.fact.functionId] = pendingAccess.function; + } + + LockScopeTracker lockScopeTracker(classifier); + std::unordered_map> heldLocksByAccess; + for (const auto& [functionKey, trackedAccesses] : trackedAccessesByFunction) + { + const llvm::Function* function = functionsById[functionKey]; + if (function == nullptr) + continue; + + std::unordered_map> functionLocks = + lockScopeTracker.collectHeldLocks(*function, trackedAccesses); + heldLocksByAccess.insert(functionLocks.begin(), functionLocks.end()); + } + + TUFacts facts; + facts.spawns = std::move(spawnFacts.spawns); + facts.entryConcurrency = std::move(spawnFacts.entryConcurrency); + + for (PendingAccess& pendingAccess : pendingAccesses) + { + const auto heldLocksIt = heldLocksByAccess.find(pendingAccess.instruction); + if (heldLocksIt != heldLocksByAccess.end()) + pendingAccess.fact.heldLocks = heldLocksIt->second; + facts.accesses.push_back(std::move(pendingAccess.fact)); + } + + return facts; + } +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/analysis/tu_facts_builder.hpp b/src/internal/analysis/tu_facts_builder.hpp new file mode 100644 index 0000000..edbca7f --- /dev/null +++ b/src/internal/analysis/tu_facts_builder.hpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "facts.hpp" + +namespace llvm +{ + class Module; +} // namespace llvm + +namespace ctrace::concurrency::internal::analysis +{ + class TUFactsBuilder + { + public: + [[nodiscard]] TUFacts build(const llvm::Module& module) const; + }; +} // namespace ctrace::concurrency::internal::analysis diff --git a/src/internal/compile_command_builder.cpp b/src/internal/compile_command_builder.cpp index 679d058..f6e2d0a 100644 --- a/src/internal/compile_command_builder.cpp +++ b/src/internal/compile_command_builder.cpp @@ -65,6 +65,7 @@ namespace ctrace::concurrency::internal removeOutputPathArgs(args); appendIfMissing(args, "-emit-llvm"); appendIfMissing(args, "-c"); + appendIfMissing(args, "-g"); args.push_back("-o"); args.push_back(outputPath.string()); if (!hasExactToken(args, request.inputFile)) diff --git a/src/internal/diagnostics/compiler_diagnostic_parser.cpp b/src/internal/diagnostics/compiler_diagnostic_parser.cpp new file mode 100644 index 0000000..27ee776 --- /dev/null +++ b/src/internal/diagnostics/compiler_diagnostic_parser.cpp @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "compiler_diagnostic_parser.hpp" + +#include "diagnostic_builder.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ctrace::concurrency::internal::diagnostics +{ + namespace + { + struct ParsedDiagnosticLine + { + SourceLocation location; + Severity severity = Severity::Error; + std::string severityLabel; + std::string message; + bool hasStructuredLocation = false; + }; + + std::string trim(std::string_view value) + { + std::size_t begin = 0; + while (begin < value.size() && + std::isspace(static_cast(value[begin])) != 0) + { + ++begin; + } + + std::size_t end = value.size(); + while (end > begin && std::isspace(static_cast(value[end - 1])) != 0) + --end; + + return std::string(value.substr(begin, end - begin)); + } + + bool parseUnsigned(std::string_view text, unsigned& out) + { + if (text.empty()) + return false; + + unsigned value = 0; + for (const char ch : text) + { + if (!std::isdigit(static_cast(ch))) + return false; + + value = value * 10u + static_cast(ch - '0'); + } + + out = value; + return true; + } + + std::vector splitLines(std::string_view text) + { + std::vector lines; + std::size_t cursor = 0; + while (cursor < text.size()) + { + const std::size_t lineEnd = text.find('\n', cursor); + if (lineEnd == std::string_view::npos) + { + lines.push_back(text.substr(cursor)); + break; + } + + lines.push_back(text.substr(cursor, lineEnd - cursor)); + cursor = lineEnd + 1; + } + + if (text.empty()) + lines.emplace_back(); + + return lines; + } + + std::string_view toSeverityToken(std::string_view value) + { + if (value == "fatal error") + return value; + if (value == "error") + return value; + if (value == "warning") + return value; + if (value == "note") + return value; + if (value == "remark") + return value; + return {}; + } + + Severity mapSeverity(std::string_view value) + { + if (value == "warning") + return Severity::Warning; + if (value == "remark" || value == "note") + return Severity::Info; + return Severity::Error; + } + + bool parseStructuredLine(std::string_view line, ParsedDiagnosticLine& parsed) + { + const std::size_t firstColon = line.find(':'); + if (firstColon == std::string_view::npos) + return false; + + const std::size_t secondColon = line.find(':', firstColon + 1); + if (secondColon == std::string_view::npos) + return false; + + const std::size_t thirdColon = line.find(':', secondColon + 1); + if (thirdColon == std::string_view::npos) + return false; + + const std::size_t fourthColon = line.find(':', thirdColon + 1); + if (fourthColon == std::string_view::npos) + return false; + + unsigned lineNumber = 0; + unsigned columnNumber = 0; + if (!parseUnsigned(line.substr(firstColon + 1, secondColon - firstColon - 1), + lineNumber) || + !parseUnsigned(line.substr(secondColon + 1, thirdColon - secondColon - 1), + columnNumber)) + { + return false; + } + + const std::string severity = + trim(line.substr(thirdColon + 1, fourthColon - thirdColon - 1)); + const std::string_view severityToken = toSeverityToken(severity); + if (severityToken.empty()) + return false; + + parsed.location.file = std::string(line.substr(0, firstColon)); + parsed.location.line = lineNumber; + parsed.location.column = columnNumber; + parsed.location.endLine = lineNumber; + parsed.location.endColumn = columnNumber; + parsed.severity = mapSeverity(severityToken); + parsed.severityLabel = severity; + parsed.message = trim(line.substr(fourthColon + 1)); + parsed.hasStructuredLocation = true; + return true; + } + + void addFallbackDiagnostic(DiagnosticReport& report, const CompileError& error, + std::string_view inputFile, std::string_view rawDiagnostics) + { + SourceLocation location; + location.file = std::string(inputFile); + + DiagnosticBuilder builder(report, RuleId::CompilerDiagnostic); + builder.primaryLocation(std::move(location)) + .severity(Severity::Error) + .message(error.message.empty() ? error.code.message() : error.message) + .property("compilerSeverity", std::string("error")) + .property("rawDiagnostic", std::string(rawDiagnostics)); + + const std::string formattedError = formatCompileError(error); + if (!formattedError.empty()) + builder.note(formattedError); + + builder.emit(); + } + + DiagnosticSummary summarize(const DiagnosticReport& report) + { + DiagnosticSummary summary; + for (const Diagnostic& diagnostic : report.diagnostics) + { + switch (diagnostic.severity) + { + case Severity::Info: + ++summary.info; + break; + case Severity::Warning: + ++summary.warning; + break; + case Severity::Error: + ++summary.error; + break; + } + } + return summary; + } + } // namespace + + DiagnosticReport parseCompilerDiagnostics(std::string_view rawDiagnostics, + const CompileError& error, std::string_view inputFile) + { + DiagnosticReport report; + + Diagnostic* currentPrimary = nullptr; + for (const std::string_view rawLine : splitLines(rawDiagnostics)) + { + const std::string line = trim(rawLine); + if (line.empty()) + continue; + + ParsedDiagnosticLine parsed; + if (!parseStructuredLine(line, parsed)) + { + if (currentPrimary != nullptr) + { + currentPrimary->notes.push_back(DiagnosticNote{.text = line}); + } + else + { + SourceLocation location; + location.file = std::string(inputFile); + DiagnosticBuilder(report, RuleId::CompilerDiagnostic) + .primaryLocation(std::move(location)) + .severity(Severity::Error) + .message(line) + .property("compilerSeverity", std::string("error")) + .property("rawDiagnostic", line) + .emit(); + currentPrimary = &report.diagnostics.back(); + } + continue; + } + + if (parsed.severityLabel == "note") + { + if (currentPrimary != nullptr) + { + currentPrimary->notes.push_back(DiagnosticNote{.text = parsed.message}); + currentPrimary->relatedLocations.push_back( + RelatedLocation{.label = "Compiler note", .location = parsed.location}); + } + else + { + DiagnosticBuilder(report, RuleId::CompilerDiagnostic) + .primaryLocation(parsed.location) + .severity(parsed.severity) + .message(parsed.message) + .property("compilerSeverity", parsed.severityLabel) + .property("rawDiagnostic", line) + .emit(); + currentPrimary = &report.diagnostics.back(); + } + continue; + } + + DiagnosticBuilder(report, RuleId::CompilerDiagnostic) + .primaryLocation(parsed.location) + .severity(parsed.severity) + .message(parsed.message) + .property("compilerSeverity", parsed.severityLabel) + .property("rawDiagnostic", line) + .emit(); + currentPrimary = &report.diagnostics.back(); + } + + if (report.diagnostics.empty()) + addFallbackDiagnostic(report, error, inputFile, rawDiagnostics); + + std::sort(report.diagnostics.begin(), report.diagnostics.end(), + [](const Diagnostic& lhs, const Diagnostic& rhs) + { + return std::tie(lhs.location.file, lhs.location.line, lhs.location.column, + lhs.message) < std::tie(rhs.location.file, rhs.location.line, + rhs.location.column, rhs.message); + }); + + for (std::size_t index = 0; index < report.diagnostics.size(); ++index) + report.diagnostics[index].id = "diag-" + std::to_string(index + 1); + + report.diagnosticsSummary = summarize(report); + return report; + } +} // namespace ctrace::concurrency::internal::diagnostics diff --git a/src/internal/diagnostics/compiler_diagnostic_parser.hpp b/src/internal/diagnostics/compiler_diagnostic_parser.hpp new file mode 100644 index 0000000..5b0427c --- /dev/null +++ b/src/internal/diagnostics/compiler_diagnostic_parser.hpp @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "coretrace_concurrency_analysis.hpp" +#include "coretrace_concurrency_error.hpp" + +#include + +namespace ctrace::concurrency::internal::diagnostics +{ + [[nodiscard]] DiagnosticReport parseCompilerDiagnostics(std::string_view rawDiagnostics, + const CompileError& error, + std::string_view inputFile); +} // namespace ctrace::concurrency::internal::diagnostics diff --git a/src/internal/diagnostics/diagnostic_builder.hpp b/src/internal/diagnostics/diagnostic_builder.hpp new file mode 100644 index 0000000..70cca59 --- /dev/null +++ b/src/internal/diagnostics/diagnostic_builder.hpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "coretrace_concurrency_analysis.hpp" + +#include "diagnostic_catalog.hpp" + +#include +#include + +namespace ctrace::concurrency::internal::diagnostics +{ + class DiagnosticBuilder + { + public: + DiagnosticBuilder(DiagnosticReport& report, RuleId ruleId) : report_(report) + { + diagnostic_.ruleId = ruleId; + + const RuleMetadata& metadata = lookupRuleMetadata(ruleId); + diagnostic_.severity = metadata.defaultSeverity; + if (metadata.primaryTaxonomy.has_value()) + { + const TaxonomyMetadata& taxonomy = *metadata.primaryTaxonomy; + diagnostic_.taxonomies.push_back(TaxonomyRef{ + .scheme = std::string(taxonomy.scheme), + .id = std::string(taxonomy.id), + .title = std::string(taxonomy.title), + }); + } + } + + DiagnosticBuilder& severity(Severity severity) + { + diagnostic_.severity = severity; + return *this; + } + + DiagnosticBuilder& confidence(ConfidenceLevel confidence) + { + diagnostic_.confidence = confidence; + return *this; + } + + DiagnosticBuilder& primaryLocation(SourceLocation location) + { + diagnostic_.location = std::move(location); + return *this; + } + + DiagnosticBuilder& relatedLocation(std::string label, SourceLocation location) + { + diagnostic_.relatedLocations.push_back( + RelatedLocation{.label = std::move(label), .location = std::move(location)}); + return *this; + } + + DiagnosticBuilder& message(std::string message) + { + diagnostic_.message = std::move(message); + return *this; + } + + DiagnosticBuilder& note(std::string text) + { + diagnostic_.notes.push_back(DiagnosticNote{.text = std::move(text)}); + return *this; + } + + DiagnosticBuilder& taxonomy(std::string scheme, std::string id, std::string title) + { + diagnostic_.taxonomies.push_back(TaxonomyRef{ + .scheme = std::move(scheme), + .id = std::move(id), + .title = std::move(title), + }); + return *this; + } + + DiagnosticBuilder& property(std::string key, DiagnosticPropertyValue value) + { + diagnostic_.properties.insert_or_assign(std::move(key), std::move(value)); + return *this; + } + + void emit() + { + report_.diagnostics.push_back(std::move(diagnostic_)); + } + + private: + DiagnosticReport& report_; + Diagnostic diagnostic_; + }; +} // namespace ctrace::concurrency::internal::diagnostics diff --git a/src/internal/diagnostics/diagnostic_catalog.cpp b/src/internal/diagnostics/diagnostic_catalog.cpp new file mode 100644 index 0000000..de2339d --- /dev/null +++ b/src/internal/diagnostics/diagnostic_catalog.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "diagnostic_catalog.hpp" + +namespace ctrace::concurrency::internal::diagnostics +{ + const RuleMetadata& lookupRuleMetadata(RuleId ruleId) + { + static const RuleMetadata kCompilerDiagnostic{ + .ruleId = RuleId::CompilerDiagnostic, + .title = "Compiler diagnostic", + .shortDescription = + "Represents a compiler-originated diagnostic captured during IR generation.", + .defaultSeverity = Severity::Error, + .primaryTaxonomy = std::nullopt, + }; + + static const RuleMetadata kDataRaceGlobal{ + .ruleId = RuleId::DataRaceGlobal, + .title = "Unsynchronized concurrent access to a shared global", + .shortDescription = + "Detects shared global accesses that can run concurrently without a common " + "recognized lock.", + .defaultSeverity = Severity::Error, + .primaryTaxonomy = + TaxonomyMetadata{ + .scheme = "CWE", + .id = "362", + .title = "Concurrent Execution using Shared Resource with Improper " + "Synchronization ('Race Condition')", + }, + }; + + switch (ruleId) + { + case RuleId::CompilerDiagnostic: + return kCompilerDiagnostic; + case RuleId::DataRaceGlobal: + return kDataRaceGlobal; + } + + return kCompilerDiagnostic; + } +} // namespace ctrace::concurrency::internal::diagnostics diff --git a/src/internal/diagnostics/diagnostic_catalog.hpp b/src/internal/diagnostics/diagnostic_catalog.hpp new file mode 100644 index 0000000..cc4821f --- /dev/null +++ b/src/internal/diagnostics/diagnostic_catalog.hpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "coretrace_concurrency_analysis.hpp" + +#include +#include + +namespace ctrace::concurrency::internal::diagnostics +{ + struct TaxonomyMetadata + { + std::string_view scheme; + std::string_view id; + std::string_view title; + }; + + struct RuleMetadata + { + RuleId ruleId = RuleId::DataRaceGlobal; + std::string_view title; + std::string_view shortDescription; + Severity defaultSeverity = Severity::Info; + std::optional primaryTaxonomy; + }; + + [[nodiscard]] const RuleMetadata& lookupRuleMetadata(RuleId ruleId); +} // namespace ctrace::concurrency::internal::diagnostics diff --git a/src/internal/reporting/report_renderer.cpp b/src/internal/reporting/report_renderer.cpp new file mode 100644 index 0000000..5be5983 --- /dev/null +++ b/src/internal/reporting/report_renderer.cpp @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "report_renderer.hpp" + +#include "internal/diagnostics/diagnostic_catalog.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace ctrace::concurrency::internal::reporting +{ + namespace + { + using internal::diagnostics::lookupRuleMetadata; + + std::string severityUpper(Severity severity) + { + switch (severity) + { + case Severity::Info: + return "INFO"; + case Severity::Warning: + return "WARNING"; + case Severity::Error: + return "ERROR"; + } + return "UNKNOWN"; + } + + std::string severityTitle(Severity severity) + { + switch (severity) + { + case Severity::Info: + return "Info"; + case Severity::Warning: + return "Warning"; + case Severity::Error: + return "Error"; + } + return "Unknown"; + } + + std::string confidenceUpper(ConfidenceLevel confidence) + { + switch (confidence) + { + case ConfidenceLevel::Low: + return "LOW"; + case ConfidenceLevel::Medium: + return "MEDIUM"; + case ConfidenceLevel::High: + return "HIGH"; + } + return "UNKNOWN"; + } + + std::string sarifLevel(Severity severity) + { + switch (severity) + { + case Severity::Info: + return "note"; + case Severity::Warning: + return "warning"; + case Severity::Error: + return "error"; + } + return "warning"; + } + + std::optional propertyString(const Diagnostic& diagnostic, + std::string_view key) + { + const auto it = diagnostic.properties.find(std::string(key)); + if (it == diagnostic.properties.end()) + return std::nullopt; + + if (const auto* value = std::get_if(&it->second)) + return *value; + return std::nullopt; + } + + std::string effectiveFile(const SourceLocation& location, const RenderContext& context) + { + if (!location.file.empty()) + return location.file; + return context.inputFile; + } + + std::string formatLocationHeader(const SourceLocation& location, + const RenderContext& context) + { + const std::string file = effectiveFile(location, context); + if (location.line != 0 && !file.empty()) + return file + ":" + std::to_string(location.line) + ":" + + std::to_string(location.column); + if (!file.empty()) + return file; + return ""; + } + + std::string renderDiagnosticBody(const Diagnostic& diagnostic) + { + std::string rendered; + llvm::raw_string_ostream stream(rendered); + stream << "[!!!" << severityTitle(diagnostic.severity) << "] " << diagnostic.message; + for (const DiagnosticNote& note : diagnostic.notes) + stream << "\n\t ↳ " << note.text; + stream << "\n"; + return rendered; + } + + std::optional firstCweIdentifier(const Diagnostic& diagnostic) + { + for (const TaxonomyRef& taxonomy : diagnostic.taxonomies) + { + if (taxonomy.scheme == "CWE") + return taxonomy.scheme + "-" + taxonomy.id; + } + return std::nullopt; + } + + llvm::json::Value toJsonValue(const DiagnosticPropertyValue& value) + { + return std::visit( + [](const auto& concreteValue) -> llvm::json::Value + { + using ValueType = std::decay_t; + if constexpr (std::is_same_v) + { + return llvm::json::Value(concreteValue); + } + else if constexpr (std::is_same_v) + { + return llvm::json::Value(static_cast(concreteValue)); + } + else if constexpr (std::is_same_v) + { + return llvm::json::Value(concreteValue); + } + else + { + llvm::json::Array items; + for (const std::string& item : concreteValue) + items.emplace_back(item); + return llvm::json::Value(std::move(items)); + } + }, + value); + } + + llvm::json::Object toJsonLocation(const SourceLocation& location, + const RenderContext& context) + { + return llvm::json::Object{ + {"file", effectiveFile(location, context)}, + {"function", location.function}, + {"startLine", static_cast(location.line)}, + {"startColumn", static_cast(location.column)}, + {"endLine", + static_cast(location.endLine == 0 ? location.line : location.endLine)}, + {"endColumn", static_cast(location.endColumn == 0 ? location.column + : location.endColumn)}, + }; + } + + llvm::json::Object toSarifLocation(const SourceLocation& location, + const RenderContext& context) + { + llvm::json::Object artifact{ + {"uri", + [&]() + { + const std::string file = effectiveFile(location, context); + if (file.empty()) + return std::string(); + + const std::filesystem::path file_path(file); + if (!context.sourceRoot.empty()) + { + std::error_code rel_ec; + const std::filesystem::path relative = + std::filesystem::relative(file_path, context.sourceRoot, rel_ec); + if (!rel_ec && !relative.empty()) + return relative.generic_string(); + } + return file_path.generic_string(); + }()}, + }; + + llvm::json::Object region; + if (location.line != 0) + { + region.insert({"startLine", static_cast(location.line)}); + region.insert({"startColumn", static_cast(location.column)}); + region.insert( + {"endLine", static_cast(location.endLine == 0 ? location.line + : location.endLine)}); + region.insert({"endColumn", + static_cast(location.endColumn == 0 ? location.column + : location.endColumn)}); + } + + llvm::json::Object physical_location{{"artifactLocation", std::move(artifact)}}; + if (!region.empty()) + physical_location.insert({"region", std::move(region)}); + + llvm::json::Object location_object{{"physicalLocation", std::move(physical_location)}}; + if (!location.function.empty()) + { + llvm::json::Array logical_locations; + logical_locations.emplace_back( + llvm::json::Object{{"name", location.function}, {"kind", "function"}}); + location_object.insert({"logicalLocations", std::move(logical_locations)}); + } + return location_object; + } + + std::string fingerprintFor(const Diagnostic& diagnostic, const RenderContext& context) + { + llvm::MD5 md5; + const auto symbol = propertyString(diagnostic, "symbol").value_or(""); + md5.update(std::string(toString(diagnostic.ruleId))); + md5.update(effectiveFile(diagnostic.location, context)); + md5.update(std::to_string(diagnostic.location.line)); + md5.update(std::to_string(diagnostic.location.column)); + md5.update(symbol); + + llvm::MD5::MD5Result result; + md5.final(result); + llvm::SmallString<32> rendered; + llvm::MD5::stringifyResult(result, rendered); + return std::string(rendered); + } + + std::string renderHuman(const DiagnosticReport& report, const RenderContext& context) + { + std::string rendered; + llvm::raw_string_ostream stream(rendered); + + stream << "Mode: " << context.mode << "\n"; + if (report.diagnostics.empty()) + { + stream << "\nDiagnostics summary: info=" << report.diagnosticsSummary.info + << ", warning=" << report.diagnosticsSummary.warning + << ", error=" << report.diagnosticsSummary.error << "\n"; + return rendered; + } + + for (const Diagnostic& diagnostic : report.diagnostics) + { + if (!diagnostic.location.function.empty()) + stream << "\nFunction: " << diagnostic.location.function << "\n"; + else + stream << "\nLocation: " << formatLocationHeader(diagnostic.location, context) + << "\n"; + stream << "\tseverity: " << severityUpper(diagnostic.severity) << "\n"; + stream << "\truleId: " << toString(diagnostic.ruleId) << "\n"; + if (const auto cwe = firstCweIdentifier(diagnostic); cwe.has_value()) + stream << "\tcwe: " << *cwe << "\n"; + if (const auto symbol = propertyString(diagnostic, "symbol"); symbol.has_value()) + stream << "\tsymbol: " << *symbol << "\n"; + if (diagnostic.location.line != 0) + { + stream << "\tat line " << diagnostic.location.line << ", column " + << diagnostic.location.column << "\n"; + } + else if (!effectiveFile(diagnostic.location, context).empty()) + { + stream << "\tat " << effectiveFile(diagnostic.location, context) + << " (line unavailable)\n"; + } + else + { + stream << "\tat source location unavailable\n"; + } + + const std::string body = renderDiagnosticBody(diagnostic); + const std::size_t body_size = body.size(); + if (body_size != 0 && body.back() == '\n') + stream << "\t" << body.substr(0, body_size - 1) << "\n"; + else + stream << "\t" << body << "\n"; + } + + stream << "\nDiagnostics summary: info=" << report.diagnosticsSummary.info + << ", warning=" << report.diagnosticsSummary.warning + << ", error=" << report.diagnosticsSummary.error << "\n"; + return rendered; + } + + llvm::json::Value renderJsonValue(const DiagnosticReport& report, + const RenderContext& context) + { + llvm::json::Array functions; + for (const FunctionSummary& function : report.functions) + { + llvm::json::Array thread_entries; + for (const std::string& entry : function.threadEntries) + thread_entries.emplace_back(entry); + + functions.emplace_back(llvm::json::Object{ + {"file", function.file.empty() ? context.inputFile : function.file}, + {"name", function.name}, + {"threadReachable", function.threadReachable}, + {"threadEntries", std::move(thread_entries)}, + {"sharedAccessCount", static_cast(function.sharedAccessCount)}, + {"protectedAccessCount", static_cast(function.protectedAccessCount)}, + {"writeAccessCount", static_cast(function.writeAccessCount)}, + {"hasDiagnostics", function.hasDiagnostics}, + }); + } + + llvm::json::Array diagnostics; + for (const Diagnostic& diagnostic : report.diagnostics) + { + llvm::json::Object details; + details.insert({"message", renderDiagnosticBody(diagnostic)}); + + llvm::json::Array notes; + for (const DiagnosticNote& note : diagnostic.notes) + notes.emplace_back(note.text); + details.insert({"notes", std::move(notes)}); + + llvm::json::Object properties; + for (const auto& [key, value] : diagnostic.properties) + properties.insert({key, toJsonValue(value)}); + details.insert({"properties", std::move(properties)}); + + llvm::json::Array related_locations; + for (const RelatedLocation& related : diagnostic.relatedLocations) + { + related_locations.emplace_back(llvm::json::Object{ + {"label", related.label}, + {"location", toJsonLocation(related.location, context)}, + }); + } + + llvm::json::Value confidence = nullptr; + if (diagnostic.confidence.has_value()) + confidence = llvm::json::Value(confidenceUpper(*diagnostic.confidence)); + + llvm::json::Value cwe = nullptr; + if (const auto cwe_value = firstCweIdentifier(diagnostic); cwe_value.has_value()) + cwe = llvm::json::Value(*cwe_value); + + diagnostics.emplace_back(llvm::json::Object{ + {"id", diagnostic.id}, + {"severity", severityUpper(diagnostic.severity)}, + {"ruleId", std::string(toString(diagnostic.ruleId))}, + {"confidence", std::move(confidence)}, + {"cwe", std::move(cwe)}, + {"location", toJsonLocation(diagnostic.location, context)}, + {"relatedLocations", std::move(related_locations)}, + {"details", std::move(details)}, + }); + } + + return llvm::json::Object{ + {"meta", + llvm::json::Object{ + {"tool", context.toolName}, + {"inputFile", context.inputFile}, + {"mode", context.mode}, + {"analysisTimeMs", context.analysisTimeMs}, + }}, + {"functions", std::move(functions)}, + {"diagnostics", std::move(diagnostics)}, + {"diagnosticsSummary", + llvm::json::Object{ + {"info", static_cast(report.diagnosticsSummary.info)}, + {"warning", static_cast(report.diagnosticsSummary.warning)}, + {"error", static_cast(report.diagnosticsSummary.error)}, + }}, + }; + } + + std::string renderJson(const DiagnosticReport& report, const RenderContext& context) + { + const llvm::json::Value document = renderJsonValue(report, context); + return llvm::formatv("{0:2}", document).str(); + } + + std::string renderSarif(const DiagnosticReport& report, const RenderContext& context) + { + std::set emitted_rules; + llvm::json::Array rules; + for (const Diagnostic& diagnostic : report.diagnostics) + { + if (!emitted_rules.insert(diagnostic.ruleId).second) + continue; + + const auto& metadata = lookupRuleMetadata(diagnostic.ruleId); + llvm::json::Array tags; + tags.emplace_back("concurrency"); + if (const auto cwe = firstCweIdentifier(diagnostic); cwe.has_value()) + tags.emplace_back(*cwe); + + rules.emplace_back(llvm::json::Object{ + {"id", std::string(toString(metadata.ruleId))}, + {"name", std::string(toString(metadata.ruleId))}, + {"shortDescription", llvm::json::Object{{"text", std::string(metadata.title)}}}, + {"fullDescription", + llvm::json::Object{{"text", std::string(metadata.shortDescription)}}}, + {"defaultConfiguration", + llvm::json::Object{{"level", sarifLevel(metadata.defaultSeverity)}}}, + {"properties", llvm::json::Object{{"tags", std::move(tags)}}}, + }); + } + + llvm::json::Array results; + for (const Diagnostic& diagnostic : report.diagnostics) + { + llvm::json::Array locations; + locations.emplace_back(toSarifLocation(diagnostic.location, context)); + + llvm::json::Array related_locations; + for (const RelatedLocation& related : diagnostic.relatedLocations) + { + llvm::json::Object related_json = toSarifLocation(related.location, context); + related_json.insert({"message", llvm::json::Object{{"text", related.label}}}); + related_locations.emplace_back(std::move(related_json)); + } + + results.emplace_back(llvm::json::Object{ + {"ruleId", std::string(toString(diagnostic.ruleId))}, + {"level", sarifLevel(diagnostic.severity)}, + {"message", llvm::json::Object{{"text", renderDiagnosticBody(diagnostic)}}}, + {"locations", std::move(locations)}, + {"relatedLocations", std::move(related_locations)}, + {"partialFingerprints", + llvm::json::Object{ + {"primaryLocationLineHash", fingerprintFor(diagnostic, context)}}}, + }); + } + + const llvm::json::Value document = llvm::json::Object{ + {"version", "2.1.0"}, + {"$schema", + "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json"}, + {"runs", + llvm::json::Array{ + llvm::json::Object{ + {"tool", + llvm::json::Object{ + {"driver", + llvm::json::Object{ + {"name", context.toolName}, + {"rules", std::move(rules)}, + }}, + }}, + {"results", std::move(results)}, + }, + }}, + }; + + return llvm::formatv("{0:2}", document).str(); + } + } // namespace + + std::string renderReport(const DiagnosticReport& report, const RenderContext& context, + OutputFormat format) + { + switch (format) + { + case OutputFormat::Human: + return renderHuman(report, context); + case OutputFormat::Json: + return renderJson(report, context); + case OutputFormat::Sarif: + return renderSarif(report, context); + } + + return renderHuman(report, context); + } +} // namespace ctrace::concurrency::internal::reporting diff --git a/src/internal/reporting/report_renderer.hpp b/src/internal/reporting/report_renderer.hpp new file mode 100644 index 0000000..63d6d7f --- /dev/null +++ b/src/internal/reporting/report_renderer.hpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "coretrace_concurrency_analysis.hpp" + +#include +#include +#include + +namespace ctrace::concurrency::internal::reporting +{ + struct RenderContext + { + std::string toolName; + std::string inputFile; + std::string mode; + std::int64_t analysisTimeMs = -1; + std::filesystem::path sourceRoot; + }; + + [[nodiscard]] std::string renderReport(const DiagnosticReport& report, + const RenderContext& context, OutputFormat format); +} // namespace ctrace::concurrency::internal::reporting From 47ba59be9f6fe96a0490a29aff703914aac5a85d Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 7 Apr 2026 02:39:40 +0900 Subject: [PATCH 3/6] test(analyzer): add coverage for single-tu concurrency diagnostics --- CMakeLists.txt | 16 ++ .../data-race/data_race_mutex_protected.c | 36 ++++ .../data-race/data_race_split_symbols.c | 36 ++++ tests/integration/cli/test_cli_cpp.cpp | 187 ++++++++++++++++++ tests/unit/test_architecture.cpp | 1 + tests/unit/test_concurrency_analysis.cpp | 159 +++++++++++++++ 6 files changed, 435 insertions(+) create mode 100644 tests/fixtures/concurrency/data-race/data_race_mutex_protected.c create mode 100644 tests/fixtures/concurrency/data-race/data_race_split_symbols.c create mode 100644 tests/unit/test_concurrency_analysis.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a59f65..0bd22a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,6 +164,22 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) add_test(NAME coretrace_concurrency_error_tests COMMAND coretrace_concurrency_error_tests) + add_executable(coretrace_concurrency_analysis_tests + tests/unit/test_concurrency_analysis.cpp + ) + + target_compile_definitions(coretrace_concurrency_analysis_tests + PRIVATE + ${CORETRACE_TEST_COMMON_DEFINITIONS} + ) + + target_link_libraries(coretrace_concurrency_analysis_tests + PRIVATE + coretrace_concurrency_analyzer_lib + ) + + add_test(NAME coretrace_concurrency_analysis_tests COMMAND coretrace_concurrency_analysis_tests) + add_executable(coretrace_concurrency_cli_cpp_tests tests/integration/cli/test_cli_cpp.cpp ) diff --git a/tests/fixtures/concurrency/data-race/data_race_mutex_protected.c b/tests/fixtures/concurrency/data-race/data_race_mutex_protected.c new file mode 100644 index 0000000..0891149 --- /dev/null +++ b/tests/fixtures/concurrency/data-race/data_race_mutex_protected.c @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Test M1: shared global protected by a recognized mutex on every access +#include + +int shared_counter = 0; +pthread_mutex_t shared_lock = PTHREAD_MUTEX_INITIALIZER; + +void* worker(void* arg) +{ + (void)arg; + + for (int i = 0; i < 1000; ++i) + { + pthread_mutex_lock(&shared_lock); + shared_counter++; + pthread_mutex_unlock(&shared_lock); + } + + return 0; +} + +int main() +{ + pthread_t first; + pthread_t second; + + pthread_create(&first, 0, worker, 0); + pthread_create(&second, 0, worker, 0); + + pthread_join(first, 0); + pthread_join(second, 0); + return 0; +} + +// EXPECT-HUMAN-DIAGNOSTICS-BEGIN +// EXPECT-HUMAN-DIAGNOSTICS-END diff --git a/tests/fixtures/concurrency/data-race/data_race_split_symbols.c b/tests/fixtures/concurrency/data-race/data_race_split_symbols.c new file mode 100644 index 0000000..8a6764e --- /dev/null +++ b/tests/fixtures/concurrency/data-race/data_race_split_symbols.c @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Test M1: one global races while a second one stays protected +#include + +int racy_counter = 0; +int safe_counter = 0; +pthread_mutex_t safe_lock = PTHREAD_MUTEX_INITIALIZER; + +void* worker(void* arg) +{ + (void)arg; + + for (int i = 0; i < 1000; ++i) + { + racy_counter++; + + pthread_mutex_lock(&safe_lock); + safe_counter++; + pthread_mutex_unlock(&safe_lock); + } + + return 0; +} + +int main() +{ + pthread_t first; + pthread_t second; + + pthread_create(&first, 0, worker, 0); + pthread_create(&second, 0, worker, 0); + + pthread_join(first, 0); + pthread_join(second, 0); + return 0; +} diff --git a/tests/integration/cli/test_cli_cpp.cpp b/tests/integration/cli/test_cli_cpp.cpp index c9315e6..720622e 100644 --- a/tests/integration/cli/test_cli_cpp.cpp +++ b/tests/integration/cli/test_cli_cpp.cpp @@ -120,6 +120,14 @@ namespace "\noutput:\n" + text); } + bool assertNotContains(const std::string& text, std::string_view unexpected, + std::string_view label) + { + return assertTrue(text.find(unexpected) == std::string::npos, + std::string(label) + " unexpectedly contains token: " + + std::string(unexpected) + "\noutput:\n" + text); + } + bool testHelpAndInputParsingErrors() { bool ok = true; @@ -128,6 +136,8 @@ namespace const RunResult result = runAnalyzer({"--help"}); ok = assertTrue(result.exitCode == 0, "--help should exit with code 0") && ok; ok = assertContains(result.output, "Usage:", "--help output") && ok; + ok = assertContains(result.output, "--analyze", "--help output") && ok; + ok = assertContains(result.output, "--format=human|json|sarif", "--help output") && ok; ok = assertContains(result.output, "--verbose", "--help output") && ok; } @@ -153,6 +163,24 @@ namespace ok; } + { + const RunResult result = + runAnalyzer({fixturePath("hello.c").string(), "--analyze", "--format=xml"}); + ok = assertTrue(result.exitCode == 1, "invalid --format should fail") && ok; + ok = assertContains(result.output, "Unsupported --format value", + "invalid --format output") && + ok; + } + + { + const RunResult result = + runAnalyzer({fixturePath("hello.c").string(), "--format=json"}); + ok = assertTrue(result.exitCode == 1, "--format without --analyze should fail") && ok; + ok = assertContains(result.output, "--format requires --analyze", + "format without analyze output") && + ok; + } + { const RunResult result = runAnalyzer({fixturePath("hello.c").string(), fixturePath("hello.cpp").string()}); @@ -177,6 +205,7 @@ namespace ok; ok = assertContains(result.output, "request.input-file:", "verbose output") && ok; ok = assertContains(result.output, "request.ir-format: ll", "verbose output") && ok; + ok = assertContains(result.output, "request.analyze: false", "verbose output") && ok; ok = assertContains(result.output, "request.extra-arg: -DFOO=1", "verbose output") && ok; ok = assertContains(result.output, "request.extra-arg: -Wall", "verbose output") && ok; @@ -199,6 +228,163 @@ namespace return ok; } + bool testAnalyzeMode() + { + bool ok = true; + + { + const RunResult result = runAnalyzer({fixturePath("hello.c").string(), "--analyze"}); + ok = assertTrue(result.exitCode == 0, "--analyze on hello.c should succeed") && ok; + ok = assertContains(result.output, "Mode: IR", "hello analyze output") && ok; + ok = assertContains(result.output, "Diagnostics summary: info=0, warning=0, error=0", + "hello analyze output") && + ok; + ok = assertNotContains(result.output, "IR compilation succeeded", + "hello analyze output should be pure human report") && + ok; + } + + { + const RunResult result = runAnalyzer( + {fixturePath("concurrency/data-race/data_race_basic.c").string(), "--analyze"}); + ok = assertTrue(result.exitCode == 0, + "--analyze on data_race_basic should not fail the CLI") && + ok; + ok = assertContains(result.output, "Function:", "race analyze output") && ok; + ok = assertContains(result.output, "ruleId: DataRaceGlobal", "race analyze output") && + ok; + ok = assertContains(result.output, "symbol: shared_counter", "race analyze output") && + ok; + ok = + assertContains(result.output, "at line 10, column 23", "race analyze output") && ok; + ok = assertContains(result.output, "Diagnostics summary: info=0, warning=0, error=", + "race analyze output") && + ok; + } + + { + const RunResult result = runAnalyzer( + {fixturePath("concurrency/data-race/data_race_mutex_protected.c").string(), + "--analyze"}); + ok = assertTrue(result.exitCode == 0, + "--analyze on mutex-protected fixture should succeed") && + ok; + ok = assertContains(result.output, "Diagnostics summary: info=0, warning=0, error=0", + "mutex-protected analyze output") && + ok; + } + + { + const RunResult result = runAnalyzer( + {fixturePath("concurrency/data-race/data_race_split_symbols.c").string(), + "--analyze"}); + ok = assertTrue(result.exitCode == 0, + "--analyze on split-symbol fixture should succeed") && + ok; + ok = assertContains(result.output, "symbol: racy_counter", + "split-symbol analyze output") && + ok; + ok = assertNotContains(result.output, "symbol: safe_counter", + "split-symbol analyze output") && + ok; + } + + { + const RunResult result = + runAnalyzer({fixturePath("concurrency/data-race/data_race_basic.c").string(), + "--analyze", "--format=json"}); + ok = assertTrue(result.exitCode == 0, + "--format=json on data_race_basic should succeed") && + ok; + ok = assertContains(result.output, "\"meta\"", "json analyze output") && ok; + ok = assertContains(result.output, "\"functions\"", "json analyze output") && ok; + ok = assertContains(result.output, "\"diagnostics\"", "json analyze output") && ok; + ok = assertContains(result.output, "\"ruleId\": \"DataRaceGlobal\"", + "json analyze output") && + ok; + ok = assertContains(result.output, "\"symbol\": \"shared_counter\"", + "json analyze output") && + ok; + ok = assertContains(result.output, "\"startLine\": 10", "json analyze output") && ok; + ok = assertContains(result.output, "\"startColumn\": 23", "json analyze output") && ok; + } + + { + const RunResult result = + runAnalyzer({fixturePath("concurrency/data-race/data_race_basic.c").string(), + "--analyze", "--format=sarif"}); + ok = assertTrue(result.exitCode == 0, + "--format=sarif on data_race_basic should succeed") && + ok; + ok = assertContains(result.output, "\"version\": \"2.1.0\"", "sarif analyze output") && + ok; + ok = assertContains(result.output, "\"runs\"", "sarif analyze output") && ok; + ok = assertContains(result.output, "\"ruleId\": \"DataRaceGlobal\"", + "sarif analyze output") && + ok; + ok = assertContains(result.output, "\"partialFingerprints\"", "sarif analyze output") && + ok; + ok = assertContains(result.output, "\"startLine\": 10", "sarif analyze output") && ok; + ok = assertContains(result.output, "\"startColumn\": 23", "sarif analyze output") && ok; + } + + { + const RunResult result = + runAnalyzer({fixturePath("invalid.c").string(), "--analyze", "--format=human"}); + ok = assertTrue(result.exitCode == 1, + "--format=human on invalid.c should fail with structured output") && + ok; + ok = assertContains(result.output, "Location:", "human compile-error output") && ok; + ok = assertContains(result.output, "tests/fixtures/invalid.c:2:1", + "human compile-error output") && + ok; + ok = assertContains(result.output, "ruleId: CompilerDiagnostic", + "human compile-error output") && + ok; + } + + { + const RunResult result = + runAnalyzer({fixturePath("invalid.c").string(), "--analyze", "--format=json"}); + ok = assertTrue(result.exitCode == 1, + "--format=json on invalid.c should fail with structured output") && + ok; + ok = assertContains(result.output, "\"ruleId\": \"CompilerDiagnostic\"", + "json compile-error output") && + ok; + ok = assertContains(result.output, "\"file\": \"", "json compile-error output") && ok; + ok = assertContains(result.output, "tests/fixtures/invalid.c", + "json compile-error output") && + ok; + ok = assertContains(result.output, "\"startLine\": 2", "json compile-error output") && + ok; + ok = assertContains(result.output, "\"startColumn\": 1", "json compile-error output") && + ok; + } + + { + const RunResult result = + runAnalyzer({fixturePath("invalid.c").string(), "--analyze", "--format=sarif"}); + ok = assertTrue(result.exitCode == 1, + "--format=sarif on invalid.c should fail with structured output") && + ok; + ok = assertContains(result.output, "\"ruleId\": \"CompilerDiagnostic\"", + "sarif compile-error output") && + ok; + ok = assertContains(result.output, "\"uri\": ", "sarif compile-error output") && ok; + ok = assertContains(result.output, "tests/fixtures/invalid.c", + "sarif compile-error output") && + ok; + ok = assertContains(result.output, "\"startLine\": 2", "sarif compile-error output") && + ok; + ok = + assertContains(result.output, "\"startColumn\": 1", "sarif compile-error output") && + ok; + } + + return ok; + } + bool testInputValidationFailuresAndBackendDiagnostics() { bool ok = true; @@ -325,6 +511,7 @@ int main() ok; ok = testHelpAndInputParsingErrors() && ok; ok = testSuccessfulCompilesAndVerboseMode() && ok; + ok = testAnalyzeMode() && ok; ok = testInputValidationFailuresAndBackendDiagnostics() && ok; ok = testPermissionRelatedInputFailures() && ok; diff --git a/tests/unit/test_architecture.cpp b/tests/unit/test_architecture.cpp index 7671293..f60a04c 100644 --- a/tests/unit/test_architecture.cpp +++ b/tests/unit/test_architecture.cpp @@ -786,6 +786,7 @@ define i32 @main() { "BC args should include input file only once") && assertTrue(hasToken(bcArgs, "-emit-llvm"), "BC args should include -emit-llvm") && assertTrue(hasToken(bcArgs, "-c"), "BC args should include -c") && + assertTrue(hasToken(bcArgs, "-g"), "BC args should include -g") && assertTrue(!hasToken(bcArgs, "-S"), "BC args should not include -S") && assertTrue(hasOutputPair(bcArgs, outputPath.string()), "BC args should include -o "); diff --git a/tests/unit/test_concurrency_analysis.cpp b/tests/unit/test_concurrency_analysis.cpp new file mode 100644 index 0000000..18a431b --- /dev/null +++ b/tests/unit/test_concurrency_analysis.cpp @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "coretrace_concurrency_analysis.hpp" +#include "coretrace_concurrency_analyzer.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using ctrace::concurrency::CompileRequest; + using ctrace::concurrency::CompileResult; + using ctrace::concurrency::DiagnosticReport; + using ctrace::concurrency::InMemoryIRCompiler; + using ctrace::concurrency::IRFormat; + using ctrace::concurrency::RuleId; + using ctrace::concurrency::SingleTUConcurrencyAnalyzer; + + std::filesystem::path fixturePath(std::string_view relativePath) + { + return std::filesystem::path(CORETRACE_PROJECT_SOURCE_DIR) / relativePath; + } + + bool assertTrue(bool condition, const std::string& message) + { + if (condition) + return true; + + std::cerr << "[FAIL] " << message << "\n"; + return false; + } + + std::optional analyzeFixture(std::string_view relativePath) + { + llvm::LLVMContext context; + InMemoryIRCompiler compiler; + + CompileRequest request; + request.inputFile = fixturePath(relativePath).string(); + request.format = IRFormat::BC; + + CompileResult compileResult = compiler.compile(request, context); + if (!compileResult.success || compileResult.module == nullptr) + { + std::cerr << "[FAIL] fixture compile failed for " << relativePath << "\n"; + return std::nullopt; + } + + SingleTUConcurrencyAnalyzer analyzer; + return analyzer.analyze(*compileResult.module); + } + + std::optional symbolOf(const ctrace::concurrency::Diagnostic& diagnostic) + { + const auto it = diagnostic.properties.find("symbol"); + if (it == diagnostic.properties.end()) + return std::nullopt; + + if (const auto* value = std::get_if(&it->second)) + return *value; + + return std::nullopt; + } + + bool hasDiagnosticForSymbol(const DiagnosticReport& report, std::string_view symbol) + { + return std::any_of(report.diagnostics.begin(), report.diagnostics.end(), + [symbol](const auto& diagnostic) + { + const std::optional value = symbolOf(diagnostic); + return value.has_value() && *value == symbol; + }); + } + + bool testDataRaceBasicIsReported() + { + const std::optional report = + analyzeFixture("tests/fixtures/concurrency/data-race/data_race_basic.c"); + if (!report.has_value()) + return false; + + return assertTrue(!report->diagnostics.empty(), "data_race_basic should report a race") && + assertTrue(hasDiagnosticForSymbol(*report, "shared_counter"), + "data_race_basic should report shared_counter") && + assertTrue(report->diagnostics.front().ruleId == RuleId::DataRaceGlobal, + "diagnostics should carry a stable rule id") && + assertTrue(report->diagnostics.front().location.line == 10, + "data_race_basic should report line 10") && + assertTrue(report->diagnostics.front().location.column == 23, + "data_race_basic should report column 23") && + assertTrue(!report->diagnostics.front().location.function.empty(), + "diagnostics should carry function names") && + assertTrue(report->diagnosticsSummary.error >= 1, + "data_race_basic should count an error diagnostic"); + } + + bool testAtomicVsNonAtomicReportsSharedState() + { + const std::optional report = + analyzeFixture("tests/fixtures/concurrency/data-race/cpp_atomic_vs_non_atomic.cpp"); + if (!report.has_value()) + return false; + + return assertTrue(!report->diagnostics.empty(), + "cpp_atomic_vs_non_atomic should report a race") && + assertTrue(hasDiagnosticForSymbol(*report, "state"), + "cpp_atomic_vs_non_atomic should report state"); + } + + bool testMutexProtectedFixtureHasNoDiagnostics() + { + const std::optional report = + analyzeFixture("tests/fixtures/concurrency/data-race/data_race_mutex_protected.c"); + if (!report.has_value()) + return false; + + return assertTrue(report->diagnostics.empty(), + "mutex-protected fixture should not report a race") && + assertTrue(report->diagnosticsSummary.error == 0, + "mutex-protected fixture should not count error diagnostics"); + } + + bool testTwoGlobalFixtureOnlyReportsRacySymbol() + { + const std::optional report = + analyzeFixture("tests/fixtures/concurrency/data-race/data_race_split_symbols.c"); + if (!report.has_value()) + return false; + + return assertTrue(!report->diagnostics.empty(), + "split-symbol fixture should report at least one race") && + assertTrue(hasDiagnosticForSymbol(*report, "racy_counter"), + "split-symbol fixture should report racy_counter") && + assertTrue(!hasDiagnosticForSymbol(*report, "safe_counter"), + "split-symbol fixture should not report safe_counter"); + } +} // namespace + +int main() +{ + bool ok = true; + + ok = testDataRaceBasicIsReported() && ok; + ok = testAtomicVsNonAtomicReportsSharedState() && ok; + ok = testMutexProtectedFixtureHasNoDiagnostics() && ok; + ok = testTwoGlobalFixtureOnlyReportsRacySymbol() && ok; + + if (!ok) + return 1; + + std::cout << "[PASS] concurrency analysis tests\n"; + return 0; +} From 6f93c17facb11c7e04ff8211a3d525cf2b52283a Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 7 Apr 2026 02:39:59 +0900 Subject: [PATCH 4/6] test(pytest): add fixture-embedded human output golden tests --- CMakeLists.txt | 18 +++ README.md | 5 +- .../concurrency/data-race/data_race_basic.c | 20 ++- .../missing-join/missing_join_multiple.c | 17 +++ tests/integration/cli/README.md | 10 +- .../cli/human_output_expectations.py | 142 ++++++++++++++++++ .../cli/test_human_output_golden.py | 77 ++++++++++ 7 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 tests/integration/cli/human_output_expectations.py create mode 100644 tests/integration/cli/test_human_output_golden.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bd22a4..af23bd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,6 +210,24 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) else() message(STATUS "Skipping cli integration tests: ctestfw module not available") endif() + + execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import pytest" + RESULT_VARIABLE CORETRACE_PYTEST_IMPORT_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + + if(CORETRACE_PYTEST_IMPORT_RESULT EQUAL 0) + add_test( + NAME coretrace_concurrency_human_output_golden_tests + COMMAND "${Python3_EXECUTABLE}" -m pytest + "${CMAKE_CURRENT_SOURCE_DIR}/tests/integration/cli/test_human_output_golden.py" + -q + ) + else() + message(STATUS "Skipping human output golden tests: pytest module not available") + endif() else() message(STATUS "Skipping cli integration tests: Python3 interpreter not found") endif() diff --git a/README.md b/README.md index 66bfb42..8f860f2 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,13 @@ Run the C++ test suite (unit + CLI integration) with CTest: ctest --test-dir build --output-on-failure ``` -Optional Python integration tests (requires `ctestfw`): +Optional Python integration tests: ```bash python3 tests/integration/cli/test_analyzer.py +python3 -m pytest tests/integration/cli/test_human_output_golden.py +CORETRACE_ANALYZER_BIN=./build/coretrace_concurrency_analyzer \ +python3 -m pytest tests/integration/cli/test_human_output_golden.py ``` ## Code style (clang-format) diff --git a/tests/fixtures/concurrency/data-race/data_race_basic.c b/tests/fixtures/concurrency/data-race/data_race_basic.c index a843c3b..1671e4e 100644 --- a/tests/fixtures/concurrency/data-race/data_race_basic.c +++ b/tests/fixtures/concurrency/data-race/data_race_basic.c @@ -14,13 +14,27 @@ void* increment(void* arg) { int main() { pthread_t t1, t2; - + pthread_create(&t1, NULL, increment, NULL); pthread_create(&t2, NULL, increment, NULL); - + pthread_join(t1, NULL); pthread_join(t2, NULL); - + printf("Counter: %d (attendu: 20000)\n", shared_counter); return 0; } + +// EXPECT-HUMAN-DIAGNOSTICS-BEGIN +// Function: increment +// severity: ERROR +// ruleId: DataRaceGlobal +// cwe: CWE-362 +// symbol: shared_counter +// at line 10, column 23 +// [!!!Error] unsynchronized concurrent access to global 'shared_counter' +// ↳ first access: read at ${REPO_ROOT}/tests/fixtures/concurrency/data-race/data_race_basic.c:10:23 in increment (thread entries: increment) +// ↳ conflicting access: write at ${REPO_ROOT}/tests/fixtures/concurrency/data-race/data_race_basic.c:10:23 in increment (thread entries: increment) +// ↳ possible conflict kinds: read/write, write/write +// ↳ no common recognized lock protects the conflicting accesses +// EXPECT-HUMAN-DIAGNOSTICS-END diff --git a/tests/fixtures/concurrency/missing-join/missing_join_multiple.c b/tests/fixtures/concurrency/missing-join/missing_join_multiple.c index 8e44720..d06a9d6 100644 --- a/tests/fixtures/concurrency/missing-join/missing_join_multiple.c +++ b/tests/fixtures/concurrency/missing-join/missing_join_multiple.c @@ -37,3 +37,20 @@ int main() { return 0; } + +// Known M1 precision limit: the analyzer is array-element-insensitive and +// therefore reports concurrent writes on the global array as a single race on +// `results`, even though each worker targets a distinct index. +// EXPECT-HUMAN-DIAGNOSTICS-BEGIN +// Function: compute +// severity: ERROR +// ruleId: DataRaceGlobal +// cwe: CWE-362 +// symbol: results +// at line 12, column 17 +// [!!!Error] unsynchronized concurrent access to global 'results' +// ↳ access: write at ${REPO_ROOT}/tests/fixtures/concurrency/missing-join/missing_join_multiple.c:12:17 in compute (thread entries: compute) +// ↳ conflicts with another concurrent invocation reachable from thread entry 'compute' +// ↳ possible conflict kinds: write/write +// ↳ no common recognized lock protects the conflicting accesses +// EXPECT-HUMAN-DIAGNOSTICS-END diff --git a/tests/integration/cli/README.md b/tests/integration/cli/README.md index 941f018..64b5dfa 100644 --- a/tests/integration/cli/README.md +++ b/tests/integration/cli/README.md @@ -5,19 +5,25 @@ This directory contains integration tests for the CLI bootstrap. Default test path (no Python dependency): C++ integration tests wired into `ctest` (`coretrace_concurrency_cli_cpp_tests` target). -Optional extended path: `ctestfw`-based Python runner. +Optional Python paths: +- `ctestfw`-based CLI runner +- `pytest`-based human-output golden tests driven by fixture comments Run: ```bash python3 tests/integration/cli/test_analyzer.py +python3 -m pytest tests/integration/cli/test_human_output_golden.py +CORETRACE_ANALYZER_BIN=./build/coretrace_concurrency_analyzer \ +python3 -m pytest tests/integration/cli/test_human_output_golden.py ``` Prerequisite: - Python environment with `ctestfw` installed. +- Python environment with `pytest` installed for the golden tests. -The script requires a built `coretrace_concurrency_analyzer` binary in either: +The Python scripts require a built `coretrace_concurrency_analyzer` binary in either: - `build-llvm20/coretrace_concurrency_analyzer` - `build/coretrace_concurrency_analyzer` diff --git a/tests/integration/cli/human_output_expectations.py b/tests/integration/cli/human_output_expectations.py new file mode 100644 index 0000000..46affd1 --- /dev/null +++ b/tests/integration/cli/human_output_expectations.py @@ -0,0 +1,142 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Helpers for fixture-embedded human-output golden tests.""" + +from __future__ import annotations + +import os +import re +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[3] +FIXTURES = ROOT / "tests" / "fixtures" + +_BIN_CANDIDATES = [ + ROOT / "build" / "coretrace_concurrency_analyzer", + ROOT / "build-llvm20" / "coretrace_concurrency_analyzer", + ROOT + / "extern-project" + / "build-llvm20" + / "_deps" + / "concurrency_analyzer-build" + / "coretrace_concurrency_analyzer", +] + +_ENV_BIN = os.environ.get("CORETRACE_ANALYZER_BIN") +if _ENV_BIN: + BIN = Path(_ENV_BIN) +else: + BIN = next((path for path in _BIN_CANDIDATES if path.exists()), _BIN_CANDIDATES[0]) + +EXPECT_HUMAN_DIAGNOSTICS_BEGIN = "EXPECT-HUMAN-DIAGNOSTICS-BEGIN" +EXPECT_HUMAN_DIAGNOSTICS_END = "EXPECT-HUMAN-DIAGNOSTICS-END" + +_BEGIN_RE = re.compile(r"^\s*//\s*" + EXPECT_HUMAN_DIAGNOSTICS_BEGIN + r"\s*$") +_END_RE = re.compile(r"^\s*//\s*" + EXPECT_HUMAN_DIAGNOSTICS_END + r"\s*$") +_COMMENT_RE = re.compile(r"^\s*// ?(.*)$") + + +def _normalize_newlines(text: str) -> str: + return text.replace("\r\n", "\n").replace("\r", "\n") + + +def discover_human_expectation_fixtures(root: Path = FIXTURES) -> list[Path]: + """Return every C/C++ fixture that embeds a human diagnostics expectation block.""" + + fixtures: list[Path] = [] + for path in sorted(root.rglob("*")): + if path.suffix not in {".c", ".cpp"}: + continue + + contents = path.read_text(encoding="utf-8") + if EXPECT_HUMAN_DIAGNOSTICS_BEGIN in contents: + fixtures.append(path) + + return fixtures + + +def extract_expected_human_diagnostics(path: Path) -> str: + """Extract and uncomment the fixture-embedded expected diagnostics block.""" + + lines = path.read_text(encoding="utf-8").splitlines() + begin_index: int | None = None + end_index: int | None = None + + for index, line in enumerate(lines): + if _BEGIN_RE.match(line): + if begin_index is not None: + raise ValueError(f"multiple {EXPECT_HUMAN_DIAGNOSTICS_BEGIN} blocks in {path}") + begin_index = index + continue + + if _END_RE.match(line): + if end_index is not None: + raise ValueError(f"multiple {EXPECT_HUMAN_DIAGNOSTICS_END} blocks in {path}") + end_index = index + + if begin_index is None or end_index is None or end_index < begin_index: + raise ValueError(f"incomplete expectation block in {path}") + + body_lines = lines[begin_index + 1 : end_index] + uncommented: list[str] = [] + for offset, line in enumerate(body_lines, start=begin_index + 2): + if not line.strip(): + uncommented.append("") + continue + + match = _COMMENT_RE.match(line) + if match is None: + raise ValueError(f"expected comment line inside expectation block at {path}:{offset}") + + uncommented.append(match.group(1)) + + return "\n".join(uncommented).rstrip("\n") + + +def extract_actual_human_diagnostics(output: str) -> str: + """Return only the diagnostics section from the human renderer output.""" + + normalized = _normalize_newlines(output) + + mode_index = normalized.find("Mode: ") + if mode_index == -1: + raise ValueError("human report is missing the 'Mode:' header") + + report = normalized[mode_index:] + body_start = report.find("\n\n") + if body_start == -1: + raise ValueError("human report is missing the blank line after the mode header") + + summary_marker = "\n\nDiagnostics summary:" + body_end = report.rfind(summary_marker) + if body_end == -1: + raise ValueError("human report is missing the diagnostics summary footer") + + body = report[body_start + 2 : body_end] + return body.rstrip("\n") + + +def normalize_human_output(text: str, repo_root: Path = ROOT) -> str: + """Normalize machine-specific bits without changing semantic output content.""" + + normalized = _normalize_newlines(text) + + repo_root_text = str(repo_root.resolve()) + repo_root_posix = repo_root.resolve().as_posix() + for candidate in {repo_root_text, repo_root_posix}: + normalized = normalized.replace(candidate, "${REPO_ROOT}") + + return normalized.rstrip("\n") + + +def run_human_analyzer(fixture_path: Path, binary: Path = BIN) -> subprocess.CompletedProcess[str]: + """Run the analyzer in human mode for a single fixture.""" + + return subprocess.run( + [str(binary), str(fixture_path), "--analyze", "--format=human"], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) diff --git a/tests/integration/cli/test_human_output_golden.py b/tests/integration/cli/test_human_output_golden.py new file mode 100644 index 0000000..c39ede5 --- /dev/null +++ b/tests/integration/cli/test_human_output_golden.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Golden tests for fixture-embedded human diagnostic expectations.""" + +from __future__ import annotations + +import difflib +import sys +from pathlib import Path + +import pytest + + +HERE = Path(__file__).resolve().parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from human_output_expectations import ( # noqa: E402 + BIN, + FIXTURES, + ROOT, + discover_human_expectation_fixtures, + extract_actual_human_diagnostics, + extract_expected_human_diagnostics, + normalize_human_output, + run_human_analyzer, +) + + +EXPECTED_FIXTURES = discover_human_expectation_fixtures() + + +def _fixture_id(path: Path) -> str: + return path.relative_to(FIXTURES).as_posix() + + +def test_human_output_fixtures_are_configured() -> None: + assert EXPECTED_FIXTURES, "no fixtures define EXPECT-HUMAN-DIAGNOSTICS blocks" + + +@pytest.mark.skipif(not BIN.exists(), reason=f"analyzer binary not found: {BIN}") +@pytest.mark.parametrize("fixture_path", EXPECTED_FIXTURES, ids=_fixture_id) +def test_human_output_matches_fixture_expectation(fixture_path: Path, tmp_path: Path) -> None: + expected = normalize_human_output(extract_expected_human_diagnostics(fixture_path), ROOT) + + result = run_human_analyzer(fixture_path) + if result.returncode not in {0, 1}: + pytest.fail( + f"unexpected analyzer exit code {result.returncode} for {fixture_path}\n" + f"stderr:\n{result.stderr}" + ) + + actual = normalize_human_output(extract_actual_human_diagnostics(result.stdout), ROOT) + if actual == expected: + return + + actual_path = tmp_path / "actual-human-diagnostics.txt" + stderr_path = tmp_path / "stderr.txt" + actual_path.write_text(actual + "\n", encoding="utf-8") + stderr_path.write_text(result.stderr, encoding="utf-8") + + diff = "\n".join( + difflib.unified_diff( + expected.splitlines(), + actual.splitlines(), + fromfile=f"{fixture_path} (expected)", + tofile=f"{fixture_path} (actual)", + lineterm="", + ) + ) + + pytest.fail( + f"human diagnostics mismatch for {fixture_path}\n" + f"{diff}\n\n" + f"actual diagnostics: {actual_path}\n" + f"stderr: {stderr_path}" + ) From a5794944d7f7e5d19d725abb65b9e037ff01498f Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 7 Apr 2026 02:43:00 +0900 Subject: [PATCH 5/6] chore(fixtures): fix missing stdio includes and normalize fixture signatures --- .../condition_variable_spurious.c | 16 +++++++++------- .../data-race/data_race_mixed_access.c | 7 ++++--- .../data-race/race_condition_check_then_use.c | 7 ++++--- .../memory-barrier/missing_memory_barrier.c | 7 ++++--- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/fixtures/concurrency/condition-variable/condition_variable_spurious.c b/tests/fixtures/concurrency/condition-variable/condition_variable_spurious.c index e83fa61..f1629b8 100644 --- a/tests/fixtures/concurrency/condition-variable/condition_variable_spurious.c +++ b/tests/fixtures/concurrency/condition-variable/condition_variable_spurious.c @@ -2,6 +2,7 @@ // Test 12: Condition variable - attente sans boucle (spurious wakeup) #include #include +#include pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; @@ -20,28 +21,29 @@ void* producer(void* arg) { void* consumer(void* arg) { pthread_mutex_lock(&mutex); - + // MAUVAIS: pas de boucle while pour gérer les spurious wakeups if (!data_ready) { // devrait être: while (!data_ready) pthread_cond_wait(&cond, &mutex); } - + // Peut se réveiller même si data_ready est encore false! int value = shared_data; printf("Consumer got: %d\n", value); - + pthread_mutex_unlock(&mutex); return NULL; } -int main() { +int main(void) +{ pthread_t t1, t2; - + pthread_create(&t1, NULL, consumer, NULL); pthread_create(&t2, NULL, producer, NULL); - + pthread_join(t1, NULL); pthread_join(t2, NULL); - + return 0; } diff --git a/tests/fixtures/concurrency/data-race/data_race_mixed_access.c b/tests/fixtures/concurrency/data-race/data_race_mixed_access.c index 1c72e48..166bc0c 100644 --- a/tests/fixtures/concurrency/data-race/data_race_mixed_access.c +++ b/tests/fixtures/concurrency/data-race/data_race_mixed_access.c @@ -2,6 +2,7 @@ // Test 2: Data race - accès mixte lecture/écriture #include #include +#include int shared_data = 0; bool ready = false; // Flag non protégé @@ -23,12 +24,12 @@ void* reader(void* arg) { int main() { pthread_t t1, t2; - + pthread_create(&t1, NULL, writer, NULL); pthread_create(&t2, NULL, reader, NULL); - + pthread_join(t1, NULL); pthread_join(t2, NULL); - + return 0; } diff --git a/tests/fixtures/concurrency/data-race/race_condition_check_then_use.c b/tests/fixtures/concurrency/data-race/race_condition_check_then_use.c index 93e7d3a..42dfba1 100644 --- a/tests/fixtures/concurrency/data-race/race_condition_check_then_use.c +++ b/tests/fixtures/concurrency/data-race/race_condition_check_then_use.c @@ -2,6 +2,7 @@ // Test 3: Race condition - check-then-use (TOCTOU) #include #include +#include int* shared_ptr = NULL; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; @@ -24,18 +25,18 @@ void* user(void* arg) { int main() { pthread_t threads[4]; - + for (int i = 0; i < 2; i++) { pthread_create(&threads[i], NULL, allocator, NULL); } for (int i = 2; i < 4; i++) { pthread_create(&threads[i], NULL, user, NULL); } - + for (int i = 0; i < 4; i++) { pthread_join(threads[i], NULL); } - + if (shared_ptr) free(shared_ptr); return 0; } diff --git a/tests/fixtures/concurrency/memory-barrier/missing_memory_barrier.c b/tests/fixtures/concurrency/memory-barrier/missing_memory_barrier.c index 072b060..c90e0a3 100644 --- a/tests/fixtures/concurrency/memory-barrier/missing_memory_barrier.c +++ b/tests/fixtures/concurrency/memory-barrier/missing_memory_barrier.c @@ -2,6 +2,7 @@ // Test 5: Missing memory barrier - visibilité des écritures #include #include +#include int data = 0; bool flag = false; // Devrait être atomique ou protégé par mémoire barrier @@ -25,12 +26,12 @@ void* consumer(void* arg) { int main() { pthread_t t1, t2; - + pthread_create(&t1, NULL, producer, NULL); pthread_create(&t2, NULL, consumer, NULL); - + pthread_join(t1, NULL); pthread_join(t2, NULL); - + return 0; } From 06ee9ef9ae1eef8a54f08c986c67fdbfd5a63b61 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 7 Apr 2026 02:47:12 +0900 Subject: [PATCH 6/6] fix(analyzer): include string in lock scope tracker header --- src/internal/analysis/lock_scope_tracker.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/internal/analysis/lock_scope_tracker.hpp b/src/internal/analysis/lock_scope_tracker.hpp index 0eb999b..482cffb 100644 --- a/src/internal/analysis/lock_scope_tracker.hpp +++ b/src/internal/analysis/lock_scope_tracker.hpp @@ -2,6 +2,7 @@ #pragma once #include +#include #include #include