diff --git a/CMakeLists.txt b/CMakeLists.txt index 52d497a..748dca1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ endif() set(LIB_SOURCES src/compilerlib/compiler.cpp src/compilerlib/emit/llvm_output.cpp + src/compilerlib/frontend/optnone_action.cpp src/compilerlib/toolchain.cpp src/compilerlib/instrumentation/alloc.cpp src/compilerlib/instrumentation/bounds.cpp diff --git a/README.md b/README.md index d3e83e2..27e70dc 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,50 @@ -# coretrace-compiler +# CoreTrace Compiler -#### BUILD +CoreTrace Compiler is a Clang/LLVM-based wrapper that can emit LLVM IR, build binaries, and optionally +instrument code with runtime checks (alloc/bounds/trace/vtable). It can run in a file-based mode or +an in-memory mode for tooling pipelines. + +## Build + +Quick build: ```zsh mkdir -p build && cd build ./build.sh ``` -### Code style (clang-format) - -- Version cible : `clang-format` 17 (utilisée dans la CI). -- Formater localement : `./scripts/format.sh` -- Vérifier sans modifier : `./scripts/format-check.sh` -- CMake : `cmake --build build --target format` ou `--target format-check` -- CI : le job GitHub Actions `clang-format` échoue si un fichier n’est pas formaté. - -#### BUILD (macOS) +macOS: ```zsh mkdir -p build && cd build cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ - -DLLVM_DIR=$(brew --prefix llvm)/lib/cmake/llvm \ - -DClang_DIR=$(brew --prefix llvm)/lib/cmake/clang \ - -DUSE_SHARED_LIB=OFF + -DLLVM_DIR=$(brew --prefix llvm)/lib/cmake/llvm \ + -DClang_DIR=$(brew --prefix llvm)/lib/cmake/clang \ + -DUSE_SHARED_LIB=OFF ``` -#### BUILD (Linux) +Linux: ```zsh mkdir -p build && cd build cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ - -DLLVM_DIR=/usr/lib/llvm-${LLVM_VERSION}/lib/cmake/llvm \ - -DClang_DIR=/usr/lib/llvm-${LLVM_VERSION}/lib/cmake/clang \ - -DCLANG_LINK_CLANG_DYLIB=ON \ - -DLLVM_LINK_LLVM_DYLIB=ON \ - -DUSE_SHARED_LIB=OFF \ + -DLLVM_DIR=/usr/lib/llvm-${LLVM_VERSION}/lib/cmake/llvm \ + -DClang_DIR=/usr/lib/llvm-${LLVM_VERSION}/lib/cmake/clang \ + -DCLANG_LINK_CLANG_DYLIB=ON \ + -DLLVM_LINK_LLVM_DYLIB=ON \ + -DUSE_SHARED_LIB=OFF \ && cmake --build build -j"$(nproc)" ``` -#### CORETRACE-COMPILER USAGE +## Code Style (clang-format) + +- Target version: `clang-format` 17 (CI uses this). +- Format locally: `./scripts/format.sh` +- Check without modifying: `./scripts/format-check.sh` +- CMake targets: `cmake --build build --target format` or `--target format-check` +- CI: the `clang-format` GitHub Actions job fails if a file is not formatted. + +## Usage ```zsh ./cc -S -emit-llvm test.cc @@ -56,32 +62,44 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ ./cc --in-mem -emit-llvm test.c ``` -Options: -- --instrument: enable instrumentation (required for --ct-* flags). -- --in-mem, --in-memory: print LLVM IR to stdout (use with -emit-llvm). -- --ct-shadow: enable shadow memory in the produced binary. -- --ct-shadow-aggressive, --ct-shadow=aggressive: enable the aggressive shadow mode. -- --ct-bounds-no-abort: do not abort on bounds errors. -- --ct-modules=trace,alloc,bounds,vtable: select which instrumentation passes to run (supports "all"). -- --ct-no-trace, --ct-trace: disable/enable function entry/exit instrumentation. -- --ct-no-alloc, --ct-alloc: disable/enable malloc/free instrumentation. -- --ct-no-bounds, --ct-bounds: disable/enable bounds checks. -- --ct-no-autofree, --ct-autofree: disable/enable auto-free on unreachable allocations. -- --ct-no-alloc-trace, --ct-alloc-trace: disable/enable malloc/free tracing logs. -- --ct-vcall-trace, --ct-no-vcall-trace: enable/disable virtual call tracing (Itanium ABI). -- --ct-vtable-diag, --ct-no-vtable-diag: enable/disable vtable diagnostics (extra warnings). - -All other arguments are forwarded to clang (e.g., -O2, -g, -I, -D, -L, -l, -std=...). -Alloc instrumentation rewrites malloc/free/calloc/realloc and basic C++ operator new/delete -(scalar/array). Sized/aligned new/delete overloads are not handled yet. -Vtable tooling (module vtable) requires C++ and an Itanium ABI (macOS/Linux). - -#### Auto-free GC scan (conservative) +## CLI Options + +Core options: +- `--instrument`: enable CoreTrace instrumentation (required for `--ct-*` flags). +- `--in-mem`, `--in-memory`: print LLVM IR to stdout (use with `-emit-llvm`). + +Instrumentation toggles: +- `--ct-modules=`: comma-separated list `trace,alloc,bounds,vtable,all`. +- `--ct-shadow`: enable shadow memory in the produced binary. +- `--ct-shadow-aggressive`, `--ct-shadow=aggressive`: aggressive shadow mode. +- `--ct-bounds-no-abort`: do not abort on bounds errors. +- `--ct-no-trace` / `--ct-trace`: disable/enable function entry/exit instrumentation. +- `--ct-no-alloc` / `--ct-alloc`: disable/enable malloc/free instrumentation. +- `--ct-no-bounds` / `--ct-bounds`: disable/enable bounds checks. +- `--ct-no-autofree` / `--ct-autofree`: disable/enable auto-free on unreachable allocations. +- `--ct-no-alloc-trace` / `--ct-alloc-trace`: disable/enable malloc/free tracing logs. +- `--ct-no-vcall-trace` / `--ct-vcall-trace`: disable/enable virtual call tracing (Itanium ABI). +- `--ct-no-vtable-diag` / `--ct-vtable-diag`: enable/disable vtable diagnostics. + +Frontend toggles: +- `--ct-optnone`: add `optnone` and `noinline` to user-defined functions. +- `--ct-no-optnone`: disable optnone injection. + +Notes: +- All other arguments are forwarded to clang (e.g. `-O2`, `-g`, `-I`, `-D`, `-L`, `-l`, `-std=...`). +- Alloc instrumentation rewrites `malloc/free/calloc/realloc` and basic C++ `operator new/delete` + (scalar/array). Sized/aligned new/delete overloads are not handled yet. +- Vtable tooling requires C++ and an Itanium ABI (macOS/Linux). +- Clang automatically adds `optnone` at `-O0`. Use `--ct-optnone` to force the attribute even when + passing `-Xclang -disable-O0-optnone`. + +## Auto-free GC Scan (Conservative) The runtime can run a conservative root scan (stack/regs/globals) to decide whether an -allocation is still reachable. This is optional and controlled by env vars. +allocation is still reachable. This is optional and controlled by environment variables. Typical usage: + ```zsh CT_AUTOFREE_SCAN=1 CT_AUTOFREE_SCAN_START=1 \ CT_AUTOFREE_SCAN_PTR=0 \ @@ -93,11 +111,13 @@ CT_DEBUG_AUTOFREE_SCAN=1 \ ``` Without GC scan (auto-free only from compile-time analysis): + ```zsh ./app ``` With GC scan (conservative root scan): + ```zsh CT_AUTOFREE_SCAN=1 CT_AUTOFREE_SCAN_START=1 \ CT_AUTOFREE_SCAN_PTR=1 \ @@ -109,62 +129,59 @@ CT_DEBUG_AUTOFREE_SCAN=1 \ ``` Environment variables (ms can be floating-point; US/NS override MS): -- CT_AUTOFREE_SCAN=1: enable conservative scanning. -- CT_AUTOFREE_SCAN_START=1: run a scan at startup and launch a periodic scan thread. -- CT_AUTOFREE_SCAN_PERIOD_MS=N: period between scans when START=1 (default: 1000ms). -- CT_AUTOFREE_SCAN_PERIOD_US=N: period between scans in microseconds. -- CT_AUTOFREE_SCAN_PERIOD_NS=N: period between scans in nanoseconds. -- CT_AUTOFREE_SCAN_BUDGET_MS=N: time budget per scan; if exceeded, no frees are performed. -- CT_AUTOFREE_SCAN_BUDGET_US=N: time budget per scan in microseconds. -- CT_AUTOFREE_SCAN_BUDGET_NS=N: time budget per scan in nanoseconds. -- CT_AUTOFREE_SCAN_STACK=0/1: scan thread stacks (default: 1). -- CT_AUTOFREE_SCAN_REGS=0/1: scan registers (default: 1). -- CT_AUTOFREE_SCAN_GLOBALS=0/1: scan globals (__DATA segments) (default: 1). -- CT_AUTOFREE_SCAN_INTERIOR=0/1: treat interior pointers as roots (default: 1). -- CT_AUTOFREE_SCAN_PTR=0/1: enable per-pointer scan before auto-free (default: 1). -- CT_DEBUG_AUTOFREE_SCAN=1: log scan activity only when a scan frees or times out. -- CT_DEBUG_AUTOFREE_SCAN=2: log every scan + per-pointer scans. - -Use-cases: -- Keep CT_AUTOFREE_SCAN_PTR=1 if you want a conservative safety check before any auto-free. -- Set CT_AUTOFREE_SCAN_PTR=0 if you want immediate auto-free for "unreachable" sites and - only rely on periodic scans. -- Disable GLOBALS (CT_AUTOFREE_SCAN_GLOBALS=0) to reduce scan cost when you see timeouts. +- `CT_AUTOFREE_SCAN=1`: enable conservative scanning. +- `CT_AUTOFREE_SCAN_START=1`: run a scan at startup and launch a periodic scan thread. +- `CT_AUTOFREE_SCAN_PERIOD_MS=N`: period between scans when START=1 (default: 1000ms). +- `CT_AUTOFREE_SCAN_PERIOD_US=N`: period between scans in microseconds. +- `CT_AUTOFREE_SCAN_PERIOD_NS=N`: period between scans in nanoseconds. +- `CT_AUTOFREE_SCAN_BUDGET_MS=N`: time budget per scan; if exceeded, no frees are performed. +- `CT_AUTOFREE_SCAN_BUDGET_US=N`: time budget per scan in microseconds. +- `CT_AUTOFREE_SCAN_BUDGET_NS=N`: time budget per scan in nanoseconds. +- `CT_AUTOFREE_SCAN_STACK=0/1`: scan thread stacks (default: 1). +- `CT_AUTOFREE_SCAN_REGS=0/1`: scan registers (default: 1). +- `CT_AUTOFREE_SCAN_GLOBALS=0/1`: scan globals (`__DATA` segments) (default: 1). +- `CT_AUTOFREE_SCAN_INTERIOR=0/1`: treat interior pointers as roots (default: 1). +- `CT_AUTOFREE_SCAN_PTR=0/1`: enable per-pointer scan before auto-free (default: 1). +- `CT_DEBUG_AUTOFREE_SCAN=1`: log scan activity only when a scan frees or times out. +- `CT_DEBUG_AUTOFREE_SCAN=2`: log every scan plus per-pointer scans. + +Use cases: +- Keep `CT_AUTOFREE_SCAN_PTR=1` for a conservative safety check before any auto-free. +- Set `CT_AUTOFREE_SCAN_PTR=0` for immediate auto-free on unreachable sites, and rely on periodic scans. +- Disable `CT_AUTOFREE_SCAN_GLOBALS=0` to reduce scan cost when you see timeouts. Notes: - This is conservative: stale values on stack/regs/globals can keep a pointer "reachable". - If a scan times out (budget exceeded), nothing is freed to avoid false positives. -Vtable diagnostics (--ct-vtable-diag): +Vtable diagnostics (`--ct-vtable-diag`): - Logs an init line with alloc-tracking state: enabled/disabled and the reason if disabled. -- Warns when vptr/vtable data is invalid: null this pointer, missing vptr, or missing typeinfo. +- Warns when vptr/vtable data is invalid: null `this` pointer, missing vptr, or missing typeinfo. - Warns when vtable address cannot be resolved to a module (dladdr/phdr/dyld fallback). - Warns on module mismatch when both vtable module and target module are resolved. - Notes partial resolution cases (only vtable or target resolved) and escalates if target is non-exec. - Warns on static vs dynamic type mismatch when a static type is available. - Warns when the object appears freed (alloc table says state=freed). -#### HOW TO USE THIS CORETRACE-COMPILER IN YOUR PROJECT - -You can integrate coretrace-compiler directly into your own CMake project using FetchContent or by building it as a standalone library. - -Below is a minimal example using the sample project located in the extern-project directory. +## Using coretrace-compiler in Your Project -##### (Please use a recent LLVM version for best compatibility : llvm@19/20/...) +You can integrate coretrace-compiler into your own CMake project using FetchContent or by building +it as a standalone library. The sample project in `extern-project` shows a minimal setup. ```zsh cd extern-project mkdir -p build && cd build cmake .. -DLLVM_DIR=$(brew --prefix llvm@20)/lib/cmake/llvm/ \ - -DClang_DIR=$(brew --prefix llvm@20)/lib/cmake/clang + -DClang_DIR=$(brew --prefix llvm@20)/lib/cmake/clang make ``` This example demonstrates how to: -• Link against compilerlib_static or compilerlib_shared -• Use the public API from include/compilerlib/ -• Build your project with the correct LLVM/Clang version (20 on macOS) +- Link against `compilerlib_static` or `compilerlib_shared`. +- Use the public API from `include/compilerlib/`. +- Build with the correct LLVM/Clang version (20 on macOS). -You can use the same pattern in any external project by passing the correct LLVM_DIR and Clang_DIR paths to CMake. +You can use the same pattern in any external project by passing the correct `LLVM_DIR` +and `Clang_DIR` paths to CMake. diff --git a/include/compilerlib/frontend/optnone_action.hpp b/include/compilerlib/frontend/optnone_action.hpp new file mode 100644 index 0000000..40be3fc --- /dev/null +++ b/include/compilerlib/frontend/optnone_action.hpp @@ -0,0 +1,40 @@ +#ifndef COMPILERLIB_FRONTEND_OPTNONE_ACTION_HPP +#define COMPILERLIB_FRONTEND_OPTNONE_ACTION_HPP + +#include +#include +#include +#include + +#include +#include + +namespace compilerlib::frontend +{ + + std::unique_ptr makeOptNoneConsumer(clang::ASTContext& ctx); + + template class OptNoneAction : public BaseAction + { + public: + using BaseAction::BaseAction; + + protected: + std::unique_ptr CreateASTConsumer(clang::CompilerInstance& CI, + llvm::StringRef InFile) override + { + auto codegen = BaseAction::CreateASTConsumer(CI, InFile); + if (!codegen) + { + return codegen; + } + std::vector> consumers; + consumers.push_back(makeOptNoneConsumer(CI.getASTContext())); + consumers.push_back(std::move(codegen)); + return std::make_unique(std::move(consumers)); + } + }; + +} // namespace compilerlib::frontend + +#endif // COMPILERLIB_FRONTEND_OPTNONE_ACTION_HPP diff --git a/include/compilerlib/instrumentation/config.hpp b/include/compilerlib/instrumentation/config.hpp index b8fc72e..4dd412d 100644 --- a/include/compilerlib/instrumentation/config.hpp +++ b/include/compilerlib/instrumentation/config.hpp @@ -26,6 +26,7 @@ namespace compilerlib bool autofree_enabled = false; bool alloc_trace_enabled = true; bool bounds_without_alloc = false; + bool optnone_enabled = false; }; void extractRuntimeConfig(const std::vector& input, diff --git a/src/cli/help.cc b/src/cli/help.cc index ba2d3b0..23a3089 100644 --- a/src/cli/help.cc +++ b/src/cli/help.cc @@ -45,6 +45,10 @@ namespace cli << " --ct-no-vcall-trace / --ct-vcall-trace\n" << " --ct-no-vtable-diag / --ct-vtable-diag\n" << "\n" + << "Frontend toggles:\n" + << " --ct-optnone Add optnone/noinline to user-defined functions.\n" + << " --ct-no-optnone Disable optnone injection.\n" + << "\n" << "Defaults:\n" << " instrumentation: off\n" << " modules: trace,alloc,bounds (vtable disabled)\n" diff --git a/src/cli/main.cc b/src/cli/main.cc index fd31baf..d12d661 100644 --- a/src/cli/main.cc +++ b/src/cli/main.cc @@ -19,9 +19,16 @@ int main(int argc, char* argv[]) } auto res = compilerlib::compile(parsed.compiler_args, parsed.mode, parsed.instrument); - if (!res.success) + if (!res.diagnostics.empty()) { std::cerr << res.diagnostics; + if (res.diagnostics.back() != '\n') + { + std::cerr << '\n'; + } + } + if (!res.success) + { return 1; } diff --git a/src/compilerlib/compiler.cpp b/src/compilerlib/compiler.cpp index 832fc75..926a6a1 100644 --- a/src/compilerlib/compiler.cpp +++ b/src/compilerlib/compiler.cpp @@ -2,6 +2,7 @@ #include "compilerlib/attributes.hpp" #include "compilerlib/toolchain.hpp" +#include "compilerlib/frontend/optnone_action.hpp" #include "compilerlib/instrumentation/alloc.hpp" #include "compilerlib/instrumentation/bounds.hpp" #include "compilerlib/instrumentation/config.hpp" @@ -32,6 +33,7 @@ #include #include +#include #include #include #include @@ -226,6 +228,8 @@ namespace compilerlib "use --ct-alloc or disable bounds\n"; ctx_.dc.os.flush(); } + warnOptnoneConflict(); + stripToMemoryArgs(); DriverConfig driverCfg; if (!resolveDriverConfig(ctx_.filtered_args, driverCfg, error)) { @@ -337,6 +341,34 @@ namespace compilerlib args.swap(out); } + static void removeArg(std::vector& args, llvm::StringRef opt) + { + args.erase(std::remove_if(args.begin(), args.end(), + [&](const std::string& arg) { return arg == opt; }), + args.end()); + } + + static void removeXclangArg(std::vector& args, llvm::StringRef opt) + { + std::vector out; + out.reserve(args.size()); + for (size_t i = 0; i < args.size(); ++i) + { + const auto& arg = args[i]; + if (arg == "-Xclang" && i + 1 < args.size() && args[i + 1] == opt) + { + ++i; + continue; + } + if (arg == opt) + { + continue; + } + out.push_back(arg); + } + args.swap(out); + } + CT_NODISCARD bool linkRequested(void) const { return !(hasArg(ctx_.filtered_args, "-c") || hasArg(ctx_.filtered_args, "-S") || @@ -344,6 +376,31 @@ namespace compilerlib hasArg(ctx_.filtered_args, "-emit-llvm")); } + void stripToMemoryArgs(void) + { + if (ctx_.mode != OutputMode::ToMemory) + { + return; + } + removeArg(ctx_.filtered_args, "-c"); + } + + void warnOptnoneConflict(void) + { + if (!ctx_.runtimeConfig.optnone_enabled) + { + return; + } + if (!hasArg(ctx_.filtered_args, "-disable-O0-optnone")) + { + return; + } + ctx_.dc.os << "warning: ct: -disable-O0-optnone ignored because --ct-optnone is " + "enabled\n"; + ctx_.dc.os.flush(); + removeXclangArg(ctx_.filtered_args, "-disable-O0-optnone"); + } + CompileContext& ctx_; }; @@ -435,66 +492,53 @@ namespace compilerlib return false; } - clang::EmitLLVMOnlyAction action; - resetDiagnostics(); - if (!ci->ExecuteAction(action)) + auto handleModule = [&](std::unique_ptr module) -> bool { - error = std::move(ctx_.dc.message); - return false; - } - - std::unique_ptr module = action.takeModule(); - if (!module) - { - error = "failed to generate LLVM module"; - return false; - } - - if (ctx_.runtimeConfig.trace_enabled) - { - instrumentModule(*module); - } - if (ctx_.runtimeConfig.alloc_enabled) - { - wrapAllocCalls(*module); - } - if (ctx_.runtimeConfig.bounds_enabled) - { - instrumentMemoryAccesses(*module); - } - if (ctx_.runtimeConfig.vtable_enabled || ctx_.runtimeConfig.vcall_trace_enabled) - { - instrumentVirtualCalls(*module, ctx_.runtimeConfig.vcall_trace_enabled, - ctx_.runtimeConfig.vtable_enabled); - } - emitRuntimeConfigGlobals(*module, ctx_.runtimeConfig); + if (ctx_.runtimeConfig.trace_enabled) + { + instrumentModule(*module); + } + if (ctx_.runtimeConfig.alloc_enabled) + { + wrapAllocCalls(*module); + } + if (ctx_.runtimeConfig.bounds_enabled) + { + instrumentMemoryAccesses(*module); + } + if (ctx_.runtimeConfig.vtable_enabled || ctx_.runtimeConfig.vcall_trace_enabled) + { + instrumentVirtualCalls(*module, ctx_.runtimeConfig.vcall_trace_enabled, + ctx_.runtimeConfig.vtable_enabled); + } + emitRuntimeConfigGlobals(*module, ctx_.runtimeConfig); - const char* outputPath = findArgValue(ccArgs, "-o"); - if (!outputPath) - { - error = "unable to determine output file"; - return false; - } - switch (actionKind) - { - case clang::frontend::EmitObj: - if (!emit::emitObjectFile(*module, *ci, outputPath, error)) - return false; - break; - case clang::frontend::EmitLLVM: - if (!emit::emitLLVMIRFile(*module, outputPath, error)) + const char* outputPath = findArgValue(ccArgs, "-o"); + if (!outputPath) + { + error = "unable to determine output file"; return false; - break; - case clang::frontend::EmitBC: - if (!emit::emitBitcodeFile(*module, outputPath, error)) + } + switch (actionKind) + { + case clang::frontend::EmitObj: + return emit::emitObjectFile(*module, *ci, outputPath, error); + case clang::frontend::EmitLLVM: + return emit::emitLLVMIRFile(*module, outputPath, error); + case clang::frontend::EmitBC: + return emit::emitBitcodeFile(*module, outputPath, error); + default: + error = "instrumentation only supports object or LLVM IR/bitcode output"; return false; - break; - default: - error = "instrumentation only supports object or LLVM IR/bitcode output"; - return false; - } + } + }; - return true; + if (ctx_.runtimeConfig.optnone_enabled) + { + return runCodegenWithModule>( + *ci, handleModule, error); + } + return runCodegenWithModule(*ci, handleModule, error); } CompileResult runSingle(const clang::driver::Command& job, @@ -528,28 +572,19 @@ namespace compilerlib { case clang::frontend::EmitObj: { - clang::EmitObjAction action; - - resetDiagnostics(); - if (!ci->ExecuteAction(action)) + if (!runFrontendAction(*ci)) return fail("compilation failed"); break; } case clang::frontend::EmitAssembly: { - clang::EmitAssemblyAction action; - - resetDiagnostics(); - if (!ci->ExecuteAction(action)) + if (!runFrontendAction(*ci)) return fail("compilation failed"); break; } case clang::frontend::EmitBC: { - clang::EmitBCAction action; - - resetDiagnostics(); - if (!ci->ExecuteAction(action)) + if (!runFrontendAction(*ci)) return fail("compilation failed"); break; } @@ -557,33 +592,13 @@ namespace compilerlib { if (ctx_.mode == OutputMode::ToFile) { - clang::EmitLLVMAction action; - - resetDiagnostics(); - if (!ci->ExecuteAction(action)) + if (!runFrontendAction(*ci)) return fail("compilation failed"); } if (ctx_.mode == OutputMode::ToMemory) { - clang::EmitLLVMOnlyAction action; - - resetDiagnostics(); - if (!ci->ExecuteAction(action)) - { - result.success = false; - if (includeDriverDiags) - { - result.diagnostics = mergeDiagnostics(ctx_.driver_diagnostics, - std::move(ctx_.dc.message)); - } - else - { - result.diagnostics = std::move(ctx_.dc.message); - } - return result; - } - std::unique_ptr module = action.takeModule(); - if (module) + std::string actionError; + auto handleModule = [&](std::unique_ptr module) -> bool { if (ctx_.instrument) { @@ -605,34 +620,53 @@ namespace compilerlib module->print(rso, nullptr); rso.flush(); result.llvmIR = std::move(llvmIR); + return true; + }; + + bool ok = false; + if (ctx_.runtimeConfig.optnone_enabled) + { + ok = runCodegenWithModule< + frontend::OptNoneAction>( + *ci, handleModule, actionError); + } + else + { + ok = runCodegenWithModule(*ci, handleModule, + actionError); + } + if (!ok) + { + result.success = false; + if (includeDriverDiags) + { + result.diagnostics = mergeDiagnostics(ctx_.driver_diagnostics, + std::move(actionError)); + } + else + { + result.diagnostics = std::move(actionError); + } + return result; } } break; } case clang::frontend::PrintPreprocessedInput: { - clang::PrintPreprocessedAction action; - - resetDiagnostics(); - if (!ci->ExecuteAction(action)) + if (!runFrontendAction(*ci)) return fail("preprocessing failed"); break; } case clang::frontend::RunPreprocessorOnly: { - clang::PreprocessOnlyAction action; - - resetDiagnostics(); - if (!ci->ExecuteAction(action)) + if (!runFrontendAction(*ci)) return fail("preprocessing failed"); break; } case clang::frontend::ParseSyntaxOnly: { - clang::SyntaxOnlyAction action; - - resetDiagnostics(); - if (!ci->ExecuteAction(action)) + if (!runFrontendAction(*ci)) return fail("parsing failed"); break; } @@ -675,6 +709,40 @@ namespace compilerlib ctx_.dc.os.flush(); } + template + CT_NODISCARD bool runCodegenWithModule(clang::CompilerInstance& ci, Handler&& handler, + std::string& error) + { + Action action; + resetDiagnostics(); + if (!ci.ExecuteAction(action)) + { + error = std::move(ctx_.dc.message); + return false; + } + std::unique_ptr module = action.takeModule(); + if (!module) + { + error = "failed to generate LLVM module"; + return false; + } + return handler(std::move(module)); + } + + template + CT_NODISCARD bool runFrontendAction(clang::CompilerInstance& ci) + { + if (ctx_.runtimeConfig.optnone_enabled) + { + frontend::OptNoneAction action; + resetDiagnostics(); + return ci.ExecuteAction(action); + } + Action action; + resetDiagnostics(); + return ci.ExecuteAction(action); + } + CT_NODISCARD std::unique_ptr makeCompilerInstance(const llvm::opt::ArgStringList& ccArgs) { @@ -861,7 +929,7 @@ namespace compilerlib return { false, ctx.dc.message.empty() ? "no jobs to run" : std::move(ctx.dc.message), {}}; - if (!instrument && mode == OutputMode::ToFile) + if (!instrument && mode == OutputMode::ToFile && !ctx.runtimeConfig.optnone_enabled) return runNonInstrumentedCompilation(ctx, driver, *comp); JobPlan plan = buildJobPlan(*comp); diff --git a/src/compilerlib/frontend/optnone_action.cpp b/src/compilerlib/frontend/optnone_action.cpp new file mode 100644 index 0000000..02851c4 --- /dev/null +++ b/src/compilerlib/frontend/optnone_action.cpp @@ -0,0 +1,78 @@ +#include "compilerlib/frontend/optnone_action.hpp" + +#include +#include +#include +#include + +namespace compilerlib::frontend +{ + namespace + { + bool shouldAnnotate(const clang::FunctionDecl& decl, clang::ASTContext& ctx) + { + clang::SourceManager& sm = ctx.getSourceManager(); + clang::SourceLocation loc = sm.getSpellingLoc(decl.getLocation()); + if (loc.isInvalid()) + return false; + if (sm.isInSystemHeader(loc) || sm.isInSystemMacro(loc)) + return false; + return true; + } + + class OptNoneConsumer : public clang::ASTConsumer + { + public: + explicit OptNoneConsumer(clang::ASTContext& ctx) : ctx_(ctx) {} + + bool HandleTopLevelDecl(clang::DeclGroupRef decls) override + { + for (clang::Decl* decl : decls) + { + annotateDecl(decl); + } + return true; + } + + void HandleInlineFunctionDefinition(clang::FunctionDecl* decl) override + { + annotateFunction(decl); + } + + private: + void annotateDecl(clang::Decl* decl) + { + if (!decl) + return; + if (auto* fn = clang::dyn_cast(decl)) + { + annotateFunction(fn); + } + } + + void annotateFunction(clang::FunctionDecl* decl) + { + if (!decl || !decl->hasBody() || decl->isImplicit()) + return; + if (!shouldAnnotate(*decl, ctx_)) + return; + if (!decl->hasAttr()) + { + decl->addAttr(clang::OptimizeNoneAttr::CreateImplicit(ctx_)); + } + if (!decl->hasAttr()) + { + decl->addAttr(clang::NoInlineAttr::CreateImplicit(ctx_)); + } + } + + clang::ASTContext& ctx_; + }; + } // namespace + + std::unique_ptr makeOptNoneConsumer(clang::ASTContext& ctx) + { + return std::make_unique(ctx); + } + +} // namespace compilerlib::frontend diff --git a/src/compilerlib/instrumentation/config.cpp b/src/compilerlib/instrumentation/config.cpp index a082ad3..5ad8458 100644 --- a/src/compilerlib/instrumentation/config.cpp +++ b/src/compilerlib/instrumentation/config.cpp @@ -103,6 +103,16 @@ namespace compilerlib config.shadow_enabled = true; continue; } + if (arg == "--ct-optnone") + { + config.optnone_enabled = true; + continue; + } + if (arg == "--ct-no-optnone") + { + config.optnone_enabled = false; + continue; + } if (arg == "--ct-shadow-aggressive") { config.shadow_enabled = true; diff --git a/test/examples/test_smoke.py b/test/examples/test_smoke.py index 51ead57..33d249a 100644 --- a/test/examples/test_smoke.py +++ b/test/examples/test_smoke.py @@ -6,6 +6,7 @@ from ctestfw.framework.testcase import TestCase from ctestfw.framework.suite import TestSuite from ctestfw.framework.reporter import ConsoleReporter +from ctestfw.assertions.core import Assertion, require from ctestfw.assertions.compiler import ( assert_exit_code, assert_argv_contains, @@ -32,6 +33,22 @@ def copy_fixtures(ws: Path, files: list[Path]) -> None: dst = ws / f.name shutil.copy2(src, dst) +def assert_file_contains(path: str, text: str) -> Assertion: + def _check(res) -> None: + p = Path(path) + if not p.is_absolute(): + p = res.run.cwd / p + require(p.exists(), f"output does not exist: {p}") + data = p.read_text(encoding="utf-8", errors="ignore") + require(text in data, f"file does not contain '{text}': {p}") + return Assertion(name=f"file_contains_{Path(path).name}", check=_check) + +def assert_stderr_contains(text: str) -> Assertion: + def _check(res) -> None: + require(text in (res.run.stderr or ""), + f"stderr does not contain '{text}'\nstderr:\n{res.run.stderr}") + return Assertion(name=f"stderr_contains_{text}", check=_check) + def main() -> int: cc_bin = (ROOT / "build" / "cc").resolve() runner = CompilerRunner(RunnerConfig(executable=cc_bin)) @@ -386,6 +403,51 @@ def base_out_assertions(out_name: str): ], ) + tc_optnone_emit_llvm = TestCase( + name="compile_optnone_emit_llvm", + plan=CompilePlan( + name="compile_optnone_emit_llvm", + sources=[Path("hello.c")], + out=None, + extra_args=["--ct-optnone", "-O1", "-S", "-emit-llvm", "-o=hello_optnone.ll"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--ct-optnone", "-O1", "-S", "-emit-llvm"]), + assert_output_exists_at("hello_optnone.ll"), + assert_output_kind_at("hello_optnone.ll", ArtifactKind.LLVM_IR_TEXT), + assert_output_nonempty_at("hello_optnone.ll"), + assert_file_contains("hello_optnone.ll", "optnone"), + ], + ) + + tc_optnone_disable_o0 = TestCase( + name="compile_optnone_disable_o0", + plan=CompilePlan( + name="compile_optnone_disable_o0", + sources=[Path("hello.c")], + out=None, + extra_args=[ + "--ct-optnone", + "-O0", + "-Xclang", + "-disable-O0-optnone", + "-S", + "-emit-llvm", + "-o", + "-", + ], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--ct-optnone", "-O0", "-Xclang", "-disable-O0-optnone"]), + assert_stdout_contains("optnone"), + assert_stderr_contains( + "warning: ct: -disable-O0-optnone ignored because --ct-optnone is enabled" + ), + ], + ) + platform = detect_platform() common_cases = [tc_o_eq, tc_d_space, tc_d_compact, tc_cpp, tc_x_cxx] instrument_cases = [ @@ -405,6 +467,8 @@ def base_out_assertions(out_name: str): tc_readme_shadow_aggr, tc_readme_vtable, tc_readme_inmem, + tc_optnone_emit_llvm, + tc_optnone_disable_o0, ] if platform.os == OS.MACOS: cases = [tc_macho, *common_cases, *instrument_cases, *readme_cases]