diff --git a/.github/workflows/concurrency.yml b/.github/workflows/concurrency.yml index 101779a..f584028 100644 --- a/.github/workflows/concurrency.yml +++ b/.github/workflows/concurrency.yml @@ -58,6 +58,9 @@ jobs: - name: Analyze uses: CoreTrace/coretrace-concurrency-analyzer@v0 with: + # The first image with Clang's headers, which ConstSuggestionCheck.cpp + # includes (CoreTrace/coretrace-concurrency-analyzer#204). + version: v0.8.0 compile-commands: build/compile_commands.json fail-on: error # A pull request from a fork gets a read-only token, which Code Scanning refuses. diff --git a/CMakeLists.txt b/CMakeLists.txt index 8539319..db2aa56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(LLVM_LINK_LLVM_DYLIB ON) find_package(LLVM REQUIRED CONFIG) +# The check of const suggestions drives Clang itself (#217). +find_package(Clang REQUIRED CONFIG) message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}") @@ -74,6 +76,11 @@ set(STACK_ANALYZER_SOURCES src/analysis/AnalyzerUtils.cpp src/analysis/CompileCommands.cpp src/analysis/ConstParamAnalysis.cpp + src/analysis/ConstSuggestionCheck.cpp + src/analysis/ConstSuggestionCode.cpp + src/analysis/ConstSuggestionFacts.cpp + src/analysis/ConstSuggestionMatch.cpp + src/analysis/ConstSuggestionPlan.cpp src/analysis/CommandInjectionAnalysis.cpp src/analysis/DuplicateIfCondition.cpp src/analysis/DynamicAlloca.cpp @@ -194,6 +201,7 @@ target_include_directories(stack_usage_analyzer_lib target_include_directories(stack_usage_analyzer_lib SYSTEM PUBLIC ${LLVM_INCLUDE_DIRS} + ${CLANG_INCLUDE_DIRS} ) # Treat external dependency headers as system includes to reduce warning noise @@ -236,6 +244,21 @@ else() coretrace::logger ) endif() +# The same Clang libraries as compilerlib links. +if(CLANG_LINK_CLANG_DYLIB) + target_link_libraries(stack_usage_analyzer_lib PUBLIC clang-cpp) +else() + target_link_libraries(stack_usage_analyzer_lib + PUBLIC + clangAST + clangBasic + clangCodeGen + clangDriver + clangFrontend + clangLex + clangSema + ) +endif() # # ===== CLI BINARY ===== # add_executable(stack_usage_analyzer diff --git a/README.md b/README.md index 5713f5d..794ffb2 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,7 @@ A model path given to one of these three options that is not a readable file sto --no-resource-cross-tu disables cross-TU resource summaries --resource-summary-cache-dir= sets cache directory for cross-TU resource summaries (default: .cache/resource-lifetime) --resource-summary-cache-memory-only keeps cross-TU summary cache in memory only (process-local, no files) +--experimental-const-suggestion-check checks each const parameter suggestion with Clang before reporting it (experimental, off by default; see "Experimental: const parameter suggestions checked with Clang") --compile-ir-cache-dir= enables dependency-aware LLVM IR compile cache for unchanged source files --compile-ir-format=bc|ll selects source compilation IR format (`bc` default, `ll` for textual LLVM IR) --timing prints compile/analysis timings to stderr, including aggregated hotspot ranking @@ -345,6 +346,7 @@ Supported keys: - `uninitialized-cross-tu` - `resource-summary-cache-dir` - `resource-summary-cache-memory-only` +- `experimental-const-suggestion-check` - `compile-ir-cache-dir` - `compile-ir-format` (`bc` or `ll`) @@ -631,6 +633,178 @@ Example run: For test files, `run_test.py` also supports per-file model selection with: `// resource-model: `. +### Experimental: const parameter suggestions checked with Clang + +`ConstParameterNotModified` finds, in the LLVM IR, the pointer and reference parameters through +which nothing is written. The IR does not show every constraint that C and C++ put on a parameter: +the suggested `const` type may not compile (a callback of the old type, or `&p` passed as `int**` +in C++), may only drop a qualifier with a warning (the same `&p` in C), or may compile and select +other code (an overload, an override, `sizeof`, `if constexpr`, `_Generic`). + +Without `--experimental-const-suggestion-check`, the default, nothing beyond the IR analysis is +checked: every suggestion is reported as the IR analysis finds it, without a check line, including +those whose suggested type does not compile, drops a qualifier, or changes the generated code. + +With `--experimental-const-suggestion-check` (or `experimental-const-suggestion-check=true` in a +config file), each suggestion is checked with Clang before it is reported. The check is +experimental: +- it judges the rewrite from what Clang compiles before and after it: its errors and warnings, + what calls and references target, what methods override, constant conditions, and the generated + code. A difference that the facts do not explain keeps the suggestion as not checked. A change + that they show removes it, even when the program would behave the same; +- it compiles each file that sees the change once as it is, for all the suggestions of an analyzed + file, and once more for each suggestion. On this repository's self-analysis, on 4 CPUs, it adds + about 25 to 30% to the wall time. + +The option is a trade-off, not a fix: with it, a valid suggestion can still be removed or kept as +not checked; without it, suggestions whose type does not compile, drops a qualifier, or changes the +generated code are reported. + +Each suggestion gets one of three verdicts, which step 3 below decides in its order: +- verified: the suggestion is kept, with a `checked ...` line. In every file that sees the change, + the rewrite compiles, adds none of the tracked warnings, changes none of the facts below, and the + generated code matches; +- refuted: the suggestion is removed. In one of these files, the rewrite adds an error or a tracked + warning, or changes what a call or a reference targets, what a method of that name overrides, an + `if constexpr` condition, or the association that a `_Generic` selection chooses; +- indeterminate: the suggestion is kept, with `not checked with Clang: `. It then rests on + the IR analysis alone: it is not shown to be valid. + +The check runs these steps: + +1. The plan. The function is found in the defining file's compilation by its symbol. Every + declaration of it that this file sees, in the file and its headers, is rewritten in memory with + the type the check tries: a pointer to const (`const T *p`; `T *const p` becomes + `const T *const p`), or a reference to const (`const T &r`, for an rvalue reference `T &&r` too): + - a type written in a macro argument is rewritten where it is written; + - a type written by a macro that writes only the parameter gets `const` before the invocation; + - a typedef of a plain pointer is replaced by the type it names, made const, with the + qualifiers written beside it; one written by a macro leaves the suggestion not checked. + + A macro definition is never rewritten, nor a macro argument that writes another parameter's type + too: the suggestion is then not checked. +2. The files. Every file that sees the change is compiled as it is and with the rewrite, with its + own compile command, at `-O0`. The files considered are the analyzed ones and, with + `--compile-commands`, every C or C++ file of the compilation database, analyzed or not. A file + sees the change when it reads a rewritten file, when its analyzed code references the function, + or, for a function outside any class with external linkage, when it names the function after + preprocessing, in its own text or in any header it includes, system headers included. An + operator's name is not an identifier: for an operator, a file sees the change when it writes the + keyword `operator`, which most C++ files do through the standard headers they include. A + suggestion on such an operator then compiles most C++ files of the compilation database as they + are and with the rewrite, which can cost far more than for a function named by an identifier, + and any of them that Clang cannot compile as it is leaves the suggestion not checked. A function + with internal linkage (`static`, anonymous namespace) and a function of the same name in another + file are two functions, whichever of them has internal linkage. Preprocessing does not read a + module that a file imports: a Clang header module, a C++20 header unit or named module, the + interface that a module implementation unit imports included. A file that imports one, and does + not see the change otherwise, leaves the suggestion not checked, unless another file refutes it. + This does not apply when the rewrite changes only the defining file and the function is not a + free function with external linkage: no other file can declare it then. +3. The judgement, in each file, in this order: + - a file that cannot be preprocessed, or does not compile as it is, cannot be checked, nor a file + whose compilation with the rewrite stops at a fatal error: not checked; + - the rewrite changes only what it plans: a rewritten declaration that does not get the type + tried, or another declaration of a rewritten file whose type changes, leaves it not checked; + - an error, or one of these warnings, that the rewrite adds: refuted. The tracked warnings are + `incompatible-pointer-types-discards-qualifiers`, `incompatible-pointer-types`, + `incompatible-function-pointer-types`, `int-conversion`, `overloaded-virtual` and + `pointer-type-mismatch` (in C, a conditional between two callbacks that the rewrite of one + makes differ). Whatever the compile command says, the check enables them and keeps the level + Clang gives them by default: a warning stays a warning under `-Werror`, and an error stays an + error; + - a compilation with the rewrite that fails without an error that the rewrite adds: not checked; + - a call or a reference that targets another function, a method of that name that overrides + other methods, an `if constexpr` condition or a `_Generic` selection that changes: refuted. + Facts that cannot be compared one to one leave the file not checked; + - a file that declares the function itself, outside the rewritten files, with its current type, + or that converts the function's address with an explicit cast, other than to `void`: not + checked; + - the symbols that the rewrite renames are paired from these facts, and from what Clang + identifies of the lambdas and local classes nested in paired functions: their members, the + specializations whose template arguments or type hold them, and the implicit members of the + classes that hold them. Every other part of such an identity must match. One left without a + pair, or two that would pair with the same symbol, leaves the file not checked; + - the generated code is compared through those pairs. It matches: verified, unless a symbol + that the rewrite renames, other than the function's own and those of the constructors that + inherit it, is only declared in the file, before and after the rewrite: nothing shows that + another file defines the renamed one (`h` becoming `h`), so not checked. + A difference that none of these facts explains: not checked. + + Once every file is judged, a symbol that the rewrite gives the function and that another file + defines, or another declaration that the rewrite merges into the function, could collide if the + files are linked together: not checked, and the reason states that assumption. The suggestion is + refuted if one file refutes it, otherwise not checked if one file or one collision leaves it + so, otherwise verified. Only a refutation stops the check of the other files. + +Generated code is compared function by function and global by global, after these +normalizations: +- debug information is not generated; +- two kinds of metadata that do not change what the program does are removed: the source position + of inline assembly (`!srcloc`), used only to report errors, and annotations (`!annotation`), + which the LLVM Language Reference defines as without effect on the program; +- alias scopes and their domains lose their descriptive names, which identify nothing: each one + stays a distinct scope; +- every other metadata, module flags included, is compared by contents, not by number; +- attribute groups are compared by contents, not by numbers; +- values are numbered, not named, and comments of the printed IR are ignored; +- the symbols that the rewrite renames are paired, as step 3 establishes them: the function's own + symbols, the constructors that inherit it, what is named inside it (lambdas, local classes, + static locals), and the specializations that its calls instantiate. A renamed symbol without a + pair is a difference. + +Two differences that a program can observe are accepted, each only once verified, and the message +lists them: +- the name that `typeid` gives a type named after the function: it holds the function's mangled + name; +- a string constant whose only change is the signature that `__PRETTY_FUNCTION__` prints. + +The check covers only: +- the suggestions it checks. A suggestion marked `not checked with Clang: ...` rests on the IR + analysis alone, and its reason says why; +- the files that see the change, with their compile commands, and the code those configurations + compile. Other configurations, other targets, code that the preprocessor excludes, and files + outside both the analysis and the compilation database are not checked. + +Its limits: +- it does not know which files are linked together, and does not decide the one-definition rule: a + possible collision between files keeps the suggestion as not checked; +- it checks the one configuration that the analysis loads for each file: a file that the + compilation database lists twice is checked with one of its commands; +- a rewrite that changes the program's behavior in a way that calls, overrides and constant + conditions do not show is kept as not checked, not removed: `sizeof(__PRETTY_FUNCTION__)`, for + example, changes with the rewrite; +- a file that sees the changes of two analyzed files is compiled as it is once for each, and a hit + of the IR cache gives no syntax tree: the check always compiles from the sources. + +A CI job that enables the option checks the projects and configurations it analyzes, and nothing +else: an analysis run without the option, by a user or by another job, is not checked. + +In this repository, `ci.yml` runs the whole `run_test.py` suite on Linux and macOS. It checks the +default behavior, and the check's tests with the option: the const-suggestion fixtures declare it +(`// analyzer-arg:`), and the checks pass it. These tests expect suggestions kept once checked, +suggestions removed, and suggestions kept as not checked, and analyze the same inputs without the +option too. They cover these fixtures, with their compile commands and the LLVM 20 of each runner; +they do not check this repository's sources, other projects or other configurations. + +The message says how the suggestion was checked: +- `checked in the analyzed files only, with their compile commands: ...`, without a compilation + database; +- `checked in every file of the compilation database that sees the change, with its compile + command: ...`, with one; +- `difference: ...`, for each accepted difference; +- `ABI: ...`, for a C++ function with external linkage: its symbol changes, so code compiled + separately must use the updated declaration and be recompiled; +- `not checked with Clang: ...`, when the check could not run or could not conclude: for example on + an LLVM IR input (`.ll`, `.bc`), which has no source, for a file of the compilation database that + cannot be preprocessed, or for a possible collision between files. The suggestion then rests on + the IR analysis alone. + +With `--verbose`, the trace gives each verdict's grounds, on `[const-suggestion]` lines: the +suggestion (its rule, its function, the parameter's position among those the source declares), the +type it tries, where the rewrite writes it, each file judged with its configuration and its +verdict, then the verdict and its reason. + ### Example Given this code: diff --git a/docs/superpowers/plans/2026-10-16-const-suggestion-check-v2.md b/docs/superpowers/plans/2026-10-16-const-suggestion-check-v2.md new file mode 100644 index 0000000..ad4815e --- /dev/null +++ b/docs/superpowers/plans/2026-10-16-const-suggestion-check-v2.md @@ -0,0 +1,3008 @@ +# Const Suggestion Check v2 — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix `ParameterDebugBinding` on `main` (P0), then rework T, the experimental Clang check of `ConstParameterNotModified` suggestions, so that each verdict is justified and tied to the exact candidate, units and configurations it checked. + +**Architecture:** P0 binds each IR argument to its own debug declaration: its type, whether the source declares it, and its position among the source parameters. T v2 then runs a fixed flow on `217-t-v2`: initial compilation and planning in the defining file, initial and modified compilations of each unit that sees the change, correspondences established from the AST, a seven-step judgement per unit, aggregation, report. Verdicts are verified, refuted or indeterminate; the IR comparator only gains the established pairs as an input. + +**Tech Stack:** C++20, LLVM and Clang 20 (IR, AST, CodeGen `GlobalDecl`), `run_test.py` fixtures and checks, `stack_usage_analyzer_unit_tests`. + +**Spec:** `docs/superpowers/specs/2026-10-16-const-suggestion-check-v2-design.md`, approved at `8b283a8`. + +**Level of detail.** P0 is planned step by step with its code, validated in a throwaway prototype (evidence below). Stages 1 to 3 and the final phase are planned to the level of tasks, files, interfaces, tests and validation criteria. The step-level code of a stage is added to this plan when that stage starts: P0 and the updated `main` are integrated first, and they change the code these stages modify. A stage starts only once the previous one is validated. Stage 1's detail was written at its start, once Task I.1 was validated; stage 2's at its start, once stage 1 was validated. + +**Evidence:** `coretrace-stack-analyzer-evidence/217/p0/` (`repro/`, `harness/`, `fixture/`, `effect/`, `prototype-v1.diff`, `prototype-v2.diff`, `prototype-v3.diff`, `ir/`, `variadic/`, `review-v3/`). + +**Execution:** native (superpowers:executing-plans), as the user chose; an independent review of P0 before its merge (Task P0.2). Progress ledger: `wt-217-v2/.superpowers/sdd/2026-10-16-const-suggestion-check-v2/progress.md`. + +## Global Constraints + +From the spec: +- T runs only with `--experimental-const-suggestion-check`. Without the option T changes nothing (§4); P0 is the only change to the rule itself, and Task P0.2 measures it. +- Three verdicts (§3): verified (kept, with the "checked …" line), refuted (removed from the public diagnostics), indeterminate (kept, with "not checked with Clang: "). An IR difference that R, O and K do not explain is indeterminate, never refuted. +- Refuted only in a unit whose plan scope is validated, on an introduced compile error, an introduced tracked warning, or a changed R, O or K fact (§5.8, steps 4 to 6). +- Facts are the closed list of §5.4: R (the declaration a call or a reference targets, unevaluated operands and instantiations included), O (what each method named like the entity overrides), K (each `if constexpr` condition, each `_Generic` association). +- Correspondences come from compilation facts only, per unit, between its initial and modified compilations. Heuristics (`withoutConst`, `variantOf`, masked encodings, template keys) have no verdict power (§5.7, §5.9). +- T never assumes that files are linked together and never decides ODR; collisions follow the table of §5.10. +- One candidate per suggestion, built once by the planner, checked and displayed (§3; D12, D13). +- Tracked warning groups: `-W`, then `-Wno-error=`, in the initial and modified compilations (§5.3, stage 2). +- The lines tied to `reliesOnCalleeConst` (#167) stay unchanged. +- Cost criterion: +10 % on the self-analysis, same binary without and with the option, order without, with, with, without (§8). Not accepted in advance. +- Out of scope: several configurations per file, a compilation shared with coretrace-compiler, a persistent cache, link groups, a rework of the comparator. + +T v1 texts stay verbatim, since tests pin them, except the changes that §6 and §7 list: +- `checked in the analyzed files only, with their compile commands: `, `checked in every file of the compilation database that sees the change, with its compile command: `, the sentence that follows them, `not checked with Clang: `; +- the reasons `a file does not compile with Clang as it is`, ` does not compile with Clang as it is`, ` cannot be preprocessed with Clang`, `the declaration is written by a macro definition`, `no source file, so whether the suggested type compiles was not verified`, ` uses the function but has no source`, `the rewritten declaration does not get the suggested type`; +- the trace line `[const-suggestion] parameter : `, with `removed, does not compile: ::` and `removed, gains a checked warning: ::` (`check_const_suggestion_removal_reason`). New trace fields go on further `[const-suggestion]` lines. + +Process: +- TDD. A new test, fixture, `run_test.py` check or unit test, and a changed expectation, are presented with their failure evidence and added only once the user authorizes them. +- Public actions (issue, push, pull request, comment, body edit, alert dismissal) only on the user's explicit go. Bodies are prepared as files and passed with `--body-file`. +- Commits authored by Hugo, conventional subjects, no co-author, no attribution line. `CLAUDE.md` is never committed. +- The user merges. #221 stays a draft and is never merged. T v1 stays an immutable reference: `217-t-v1` at `2a5b0c1`, `coretrace-stack-analyzer-evidence/217/archive/t-v1-2a5b0c1.bundle`. +- Worktrees under `coretrace-stack-analyzer-work/`; evidence under `coretrace-stack-analyzer-evidence/217/`, never in `/tmp`. The untracked files of the main checkout stay as they are. +- Shared Linux machines (WSL): long runs under `nohup`; never signal a process that is not ours; other jobs, containers and images stay as they are. + +Commands (macOS): +- Configure: `cmake -S . -B build -G Ninja -DLLVM_DIR=$(brew --prefix llvm@20)/lib/cmake/llvm -DClang_DIR=$(brew --prefix llvm@20)/lib/cmake/clang -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DBUILD_ANALYZER_UNIT_TESTS=ON` +- Build: `cmake --build build --target stack_usage_analyzer stack_usage_analyzer_unit_tests ownership_engine_unit_tests -j 8` +- Unit tests: `./build/stack_usage_analyzer_unit_tests . && ./build/ownership_engine_unit_tests` +- Suite: `python3 run_test.py --jobs 8 --no-cache` (build with Z3) +- Format: `$(brew --prefix llvm@20)/bin/clang-format -i ` (20.1.2) + +## Review Focus + +1. **Windows targets (Microsoft C++ ABI).** A constructor of a class with a virtual base takes `is_most_derived` after the source parameters, or second when it is variadic; `main` types it as the last source parameter and reports it (`double *is_most_derived`). It must be artificial, and the other parameters keep their types. Pinned in Task P0.1 (unit test, `--target=x86_64-pc-windows-msvc`). +2. **C++23 explicit object parameter.** Clang marks `self` an object pointer, not artificial, and the source declares it: in `pick(this const Picker& self, int* p)`, `p` has source index 1, and T rewrites `p`. Pinned in Task P0.1 (unit test) and Task 1.2 (`const-suggestion-explicit-object.cpp` unchanged). +3. **A function defined in a header that several analyzed files include** (`static inline` in C, `inline` in C++): one entity in each unit, never a collision with itself, verified when each unit verifies it. Pinned in Task 1.6 (new `run_test.py` check). +4. **A file that the compilation database lists twice, with different commands.** The loader keeps one; T checks only that configuration, never claims the other, and the trace names it. Pinned in Task 1.1 (new `run_test.py` check). +5. **`-Werror=` on a tracked group**, not plain `-Werror`: the group stays a warning in both compilations, and an introduced warning still refutes. Pinned in Task 2.1 (a `run_test.py` check, stage 2, ruling 2). + +--- + +## File structure + +P0, from `main`: + +| File | Responsibility | +|---|---| +| `include/analysis/ParameterDebugBinding.hpp` | `ParameterDebugBinding::sourceIndex`. | +| `src/analysis/ParameterDebugBinding.cpp` | Type and artificiality from the parameter's own declaration; the hidden return argument recognized by its attribute; source index. | +| `test/unit/analyzer_module_unit_tests.cpp` | `testParameterDebugBinding`. | +| `test/unit/parameter_binding_input.cpp`, `test/unit/parameter_binding_block_input.c` | Its inputs. | +| `test/pointer_reference-const_correctness/virtual-base-vtt.cpp` | What the rule reports. | + +T v2, on `217-t-v2`, starting from T v1's files: + +| File | Responsibility | +|---|---| +| `src/analysis/ConstSuggestionCheck.cpp` | Flow (§5, steps 1 to 5), units and their configurations, judgement, aggregation, report, trace. | +| `src/analysis/ConstSuggestionPlan.cpp` (new) | Planning (§5.5): entity and source identity, parameter, candidate, edits, plan scope. | +| `src/analysis/ConstSuggestionFacts.cpp` (new) | Inspection of one compilation (§5.3, §5.4, §5.6): declarations, R, O and K facts and their keys, defined symbols and linkage, explicit address casts, diagnostics, scope after the rewrite. | +| `src/analysis/ConstSuggestionMatch.cpp` (new) | Correspondences (§5.7) and collisions (§5.10). | +| `src/analysis/ConstSuggestionCode.cpp` | Comparator; one new input, the established pairs (§5.9). | +| `src/analysis/ConstSuggestionInternal.hpp` | Types the files above share. | +| `include/analysis/ConstSuggestionCheck.hpp` | `checkConstSuggestions` unchanged; `rewrittenCodeDifference` takes the pairs. | +| `src/analyzer/DiagnosticEmitter.cpp` | Lines of the three verdicts; displayed candidate (D12, D13). | +| `README.md` | D3 sentence corrected; limits of §10. | + +T v1's `ConstSuggestionCheck.cpp` holds 1 267 lines; the split follows the responsibilities of §5, which require no class or interface of their own. + +--- + +## Stage P0 — `ParameterDebugBinding`, its own issue and pull request from `main` + +Root cause, at `main` `d1d9271`. At `-O0`, `buildDebugParamLayout` indexes the function's `DISubroutineType` with the debug number (`arg:`) of the argument's spilled declaration. That number counts every parameter; the subroutine type lists `this` and a block's descriptor, but not what the C++ ABI adds: the `vtt` of a base-object constructor (C2) of a class with a virtual base, after `this`, or `is_most_derived` under the Microsoft ABI, last, or second in a variadic constructor. Artificiality is read from the entry found there, not from the parameter's own declaration. Hence: +- `vtt` gets the type of the first source parameter, and is reported (`int *vtt`); +- the parameters after it get the type of the next one (`double *x` for `int* x`); +- a constructor that calls such a C2 trusts the shifted type as a `const` contract (#167): `Leaf::Leaf(int* p, …) : Writer(p, …)` reports `p` as "passed where a pointer to const is expected" although `Writer` writes through it; +- `is_most_derived` (an `int`) is reported as `double *is_most_derived`; +- a block's `.block_descriptor` is reported as ` *.block_descriptor`. + +The fix takes each spilled parameter's type and artificiality from its own declaration, the one `spilledParameter` finds; this alone binds every case above. The subroutine type stays a fallback for a declaration that gives no type, only where its entries are established, and for arguments without a declaration, the hidden return argument being recognized by its sret attribute wherever the ABI puts it. A first version left the null entry that ends a variadic function's type out of the positional count instead: the independent review showed it misbound optimized variadic functions, and it was reverted. A first prototype reconciled positions in the subroutine type instead (`evidence/217/p0/prototype-v2.diff`); it was dropped once the direct declaration was shown to bind every case, with no output change on the 424 fixtures. + +Reproductions: `evidence/217/p0/repro/` (`main-d1d9271.txt`, `caller-main-d1d9271.txt`, `block-main-d1d9271.txt`). + +### Task P0.1: bind each parameter to its own declaration + +**Files:** +- Create: `test/unit/parameter_binding_input.cpp`, `test/unit/parameter_binding_block_input.c`, `test/unit/parameter_binding_unknown_input.ll` (Step 12) +- Create: `test/pointer_reference-const_correctness/virtual-base-vtt.cpp` +- Modify: `test/unit/analyzer_module_unit_tests.cpp` (includes, new test, its call in `main`) +- Modify: `include/analysis/ParameterDebugBinding.hpp`, `src/analysis/ParameterDebugBinding.cpp` + +**Interfaces:** +- Consumes: `loadModuleForAnalysis` through the unit tests' `loadModuleFromSource`; `AnalysisConfig::extraCompileArgs`. +- Produces, used by Task 1.2: + +```cpp +struct ParameterDebugBinding +{ + // ...existing fields... + // The parameter's position among those the source declares, from 0. None for one the + // source does not declare (`this`, vtt, a block's descriptor), or that -O0 code does not + // spill. + std::optional sourceIndex; + // ...existing fields... +}; +``` + +- [ ] **Step 1: Open the issue and the branch.** After the user's go: an issue labelled `bug`, titled `ConstParameterNotModified: the vtt of a base-object constructor is reported and shifts the parameters after it`, body prepared in `evidence/217/p0/issue-body.md` (root cause and reproductions above). Then: + +```bash +R=/Users/hugopayet/Desktop/CLaude/coretrace-stack-analyzer +git -C $R fetch origin +git -C $R worktree add /Users/hugopayet/Desktop/CLaude/coretrace-stack-analyzer-work/wt--binding -b -parameter-debug-binding origin/main +``` + +- [ ] **Step 2: Write the unit test inputs.** + +`test/unit/parameter_binding_input.cpp`: + +```cpp +// SPDX-License-Identifier: Apache-2.0 +// Parameters for resolveParameterDebugBinding: a free function, a method, the constructors of a +// class with a virtual base, and a method with an explicit object parameter. Compiled with +// -std=c++23, once for each C++ ABI: under the Itanium ABI, the base-object constructor (C2) +// takes a vtt after `this`; under the Microsoft ABI, the constructor takes is_most_derived last. + +struct Base +{ + int seed; +}; + +struct Reader : virtual Base +{ + Reader(int* x, double* y); + long sum(int* p, double* q); +}; + +Reader::Reader(int* x, double* y) +{ + seed = *x + static_cast(*y); +} + +long Reader::sum(int* p, double* q) +{ + return *p + static_cast(*q); +} + +long free_function(int* a, double* b) +{ + return *a + static_cast(*b); +} + +// An explicit object parameter is declared in the source: p is the second parameter. +struct Picker +{ + int pick(this const Picker& self, int* p) + { + return *p; + } +}; + +int use_picker(int* p) +{ + return Picker{}.pick(p); +} +``` + +`test/unit/parameter_binding_block_input.c`: + +```c +// SPDX-License-Identifier: Apache-2.0 +// A block's invoke function takes the block first, as `.block_descriptor`: an artificial +// parameter that, unlike vtt, the debug type of the function lists. Compiled with -fblocks. + +int call_block(int (^block)(int*, double*), int* a, double* b) +{ + return block(a, b); +} + +int use_block(int* a, double* b) +{ + return call_block(^(int* x, double* y) { return *x + (int)*y; }, a, b); +} +``` + +- [ ] **Step 3: Add the unit test, without source indexes.** In `test/unit/analyzer_module_unit_tests.cpp`, add `#include "analysis/ParameterDebugBinding.hpp"` after `#include "analysis/IntRanges.hpp"`, and `#include ` after `#include `. Add, in the anonymous namespace, after `testCompileIRCacheWithoutCompilerRevision`: + +```cpp + /// resolveParameterDebugBinding: each parameter gets its own type. The parameters the source + /// does not declare are artificial: `this`, the vtt that the base-object constructor (C2) of a + /// class with a virtual base takes, is_most_derived under the Microsoft ABI, a block's + /// descriptor. + bool testParameterDebugBinding(const std::filesystem::path& repoRoot, TestReport& report) + { + using namespace ctrace::stack::analysis; + const auto describe = [](const llvm::Function& F) + { + std::vector parameters; + for (const llvm::Argument& Arg : F.args()) + { + const ParameterDebugBinding binding = resolveParameterDebugBinding(F, Arg); + std::string text = binding.name + ": "; + if (binding.isArtificial) + { + text += "artificial"; + } + else + { + const auto* pointer = llvm::dyn_cast_or_null(binding.type); + const llvm::DIType* pointee = pointer ? pointer->getBaseType() : nullptr; + text += (pointee ? pointee->getName().str() : "?") + " *"; + } + parameters.push_back(text); + } + return parameters; + }; + const auto check = [&](const LoadedModule& loaded, const char* symbol, + const std::vector& expected) + { + const llvm::Function* F = loaded.module->getFunction(symbol); + const std::vector actual = F ? describe(*F) : std::vector{}; + std::string text; + for (const std::string& parameter : actual) + text += " [" + parameter + "]"; + report.expect(actual == expected, + std::string("ParameterDebugBinding: ") + symbol + text); + }; + const auto load = + [&](const char* input, std::vector compileArgs, LoadedModule& out) + { + ctrace::stack::AnalysisConfig config; + config.extraCompileArgs = std::move(compileArgs); + std::string loadError; + if (loadModuleFromSource(repoRoot / "test/unit" / input, config, out, loadError)) + return true; + report.expect(false, + "ParameterDebugBinding setup: failed to load module: " + loadError); + return false; + }; + + LoadedModule itanium; + LoadedModule microsoft; + LoadedModule blocks; + if (!load("parameter_binding_input.cpp", {"-std=c++23"}, itanium) || + !load("parameter_binding_input.cpp", {"-std=c++23", "--target=x86_64-pc-windows-msvc"}, + microsoft) || + !load("parameter_binding_block_input.c", {"-fblocks"}, blocks)) + return false; + check(itanium, "_Z13free_functionPiPd", {"a: int *", "b: double *"}); + check(itanium, "_ZN6Reader3sumEPiPd", {"this: artificial", "p: int *", "q: double *"}); + check(itanium, "_ZN6ReaderC1EPiPd", {"this: artificial", "x: int *", "y: double *"}); + check(itanium, "_ZN6ReaderC2EPiPd", + {"this: artificial", "vtt: artificial", "x: int *", "y: double *"}); + check(itanium, "_ZNH6Picker4pickERKS_Pi", {"self: artificial", "p: int *"}); + check(microsoft, "??0Reader@@QEAA@PEAHPEAN@Z", + {"this: artificial", "x: int *", "y: double *", "is_most_derived: artificial"}); + check(blocks, "__use_block_block_invoke", + {".block_descriptor: artificial", "x: int *", "y: double *"}); + + // What the rule displays for x does not depend on the constructor variant. + const llvm::Function* complete = itanium.module->getFunction("_ZN6ReaderC1EPiPd"); + const llvm::Function* base = itanium.module->getFunction("_ZN6ReaderC2EPiPd"); + if (complete && base) + { + const ParameterDebugBinding inComplete = + resolveParameterDebugBinding(*complete, *complete->getArg(1)); + const ParameterDebugBinding inBase = + resolveParameterDebugBinding(*base, *base->getArg(2)); + report.expect(inComplete.name == inBase.name && inComplete.type == inBase.type && + inComplete.line == inBase.line && + inComplete.column == inBase.column && + inComplete.confidence == inBase.confidence, + "ParameterDebugBinding: x binds alike in C1 and C2"); + } + return report.failures == 0; + } +``` + +In `main`, after `(void)testLinkerSymbolName(report);`, add `(void)testParameterDebugBinding(repoRoot, report);`. + +- [ ] **Step 4: Add the fixture.** `test/pointer_reference-const_correctness/virtual-base-vtt.cpp`: + +```cpp +// SPDX-License-Identifier: Apache-2.0 +// The base-object constructor (C2) of a class with a virtual base takes a vtt after `this`: a +// parameter that the source does not declare and the debug type of the function does not list. +// ConstParameterNotModified must not report it, nor give a parameter after it the type of the +// next one, in that constructor or in the constructors that call it. + +struct Base +{ + int seed; +}; + +struct Reader : virtual Base +{ + Reader(int* x, double* y); +}; + +// x is only read: both variants, C1 and C2, report it with its own type. +// not contains: current type: double *x +// at line 24, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'x' in function 'Reader::Reader(int*, double*)' is never used to modify the pointed object +// ↳ current type: int *x +// ↳ suggested type: const int *x +Reader::Reader(int* x, double* y) +{ + seed = *x; + *y = 0; +} + +// Writer writes through p, so Leaf, which passes its own p there, must not report it. +struct Writer : virtual Base +{ + Writer(int* p, const double* q); +}; + +struct Leaf : Writer +{ + Leaf(int* p, const double* q); +}; + +Writer::Writer(int* p, const double* q) +{ + *p = static_cast(*q); +} + +// not contains: parameter 'p' in function 'Leaf::Leaf(int*, double const*)' +Leaf::Leaf(int* p, const double* q) : Writer(p, q) {} + +// not contains: parameter 'vtt' +``` + +`// not contains:` lines must not follow an `// at line` block directly: `run_test.py` would read them as part of the block. + +- [ ] **Step 5: Build and run, expect RED.** + +Run: the build command, `./build/stack_usage_analyzer_unit_tests . 2>&1 | grep ParameterDebugBinding`, then `python3 run_test.py --jobs 8 --no-cache 2>&1 | grep -A3 virtual-base-vtt`. + +Expected (recorded on `d1d9271` in `evidence/217/p0/harness/red-unit-runtime-main.txt` and `evidence/217/p0/fixture/red-fixture-main.txt`): + +``` +[FAIL] ParameterDebugBinding: _ZN6ReaderC2EPiPd [this: artificial] [vtt: int *] [x: double *] [y: double *] +[FAIL] ParameterDebugBinding: ??0Reader@@QEAA@PEAHPEAN@Z [this: artificial] [x: int *] [y: double *] [is_most_derived: double *] +[FAIL] ParameterDebugBinding: __use_block_block_invoke [.block_descriptor: ? *] [x: int *] [y: double *] +[FAIL] ParameterDebugBinding: x binds alike in C1 and C2 +``` + +The free function, the method, C1 and `Picker::pick` pass. The fixture finds its three forbidden texts (`current type: double *x`, Leaf's `p`, `vtt`) in both passes, default and smt-z3. + +- [ ] **Step 6: Commit.** + +```bash +git add test/unit/analyzer_module_unit_tests.cpp test/unit/parameter_binding_input.cpp test/unit/parameter_binding_block_input.c test/pointer_reference-const_correctness/virtual-base-vtt.cpp +git commit -m "test(const-param): an artificial parameter is neither reported nor shifts the others" +``` + +- [ ] **Step 7: Add the Clang cases the user authorized, one commit each.** A variadic function's debug type ends with a null entry for `...`; an empty parameter takes no IR argument but keeps its debug declaration; a variadic function can return a struct through a hidden argument. + +Step 7a, variadic constructors. Append to `test/unit/parameter_binding_input.cpp`: + +```cpp +// A variadic constructor: the debug type of the function ends with a null entry for `...`, which +// no parameter takes. Under the Microsoft ABI, is_most_derived comes second. +struct Varargs : virtual Base +{ + Varargs(int* x, double* y, ...); +}; + +Varargs::Varargs(int* x, double* y, ...) +{ + seed = *x + static_cast(*y); +} + +``` + +In `testParameterDebugBinding`, give `describe` a branch for a parameter without a type, between the artificial branch and the pointer one: + +```cpp + else if (!binding.type) + { + text += "no type"; + } +``` + +and add, after the block's check: + +```cpp + // A variadic function's debug type ends with a null entry for `...`, which no parameter + // takes. + check(itanium, "_ZN7VarargsC2EPiPdz", + {"this: artificial", "vtt: artificial", "x: int *", "y: double *"}); + check(microsoft, "??0Varargs@@QEAA@PEAHPEANZZ", + {"this: artificial", "is_most_derived: artificial", "x: int *", "y: double *"}); +``` + +Expected RED, besides the four failures of Step 5: + +``` +[FAIL] ParameterDebugBinding: _ZN7VarargsC2EPiPdz [this: artificial] [vtt: int *] [x: double *] [y: no type] +[FAIL] ParameterDebugBinding: ??0Varargs@@QEAA@PEAHPEANZZ [this: artificial] [is_most_derived: int *] [x: double *] [y: no type] +``` + +Commit: `test(const-param): a variadic constructor keeps the types of its parameters` (the input and the test file). + +Step 7b, an empty parameter. Append to the input: + +```cpp +// An empty parameter takes no IR argument, but keeps its debug declaration and its number. +struct Tag +{ +}; + +struct Tagged : virtual Base +{ + Tagged(int* x, Tag tag); +}; + +Tagged::Tagged(int* x, Tag tag) +{ + (void)tag; + seed = *x; +} + +``` + +and, after the variadic checks: `check(itanium, "_ZN6TaggedC2EPi3Tag", {"this: artificial", "vtt: artificial", "x: int *"});`; the comment above the variadic checks gains `; an empty parameter takes no IR argument, but keeps its debug declaration`. Expected RED: `[FAIL] ParameterDebugBinding: _ZN6TaggedC2EPi3Tag [this: artificial] [vtt: int *] [x: ? *]`. Commit: `test(const-param): an empty parameter shifts no other parameter`. + +Step 7c, a variadic hidden return. Append to the input: + +```cpp +// A variadic function that returns a struct through a hidden argument. +struct Big +{ + long values[8]; +}; + +Big make_big(int* p, ...) +{ + Big big{}; + big.values[0] = *p; + return big; +} +``` + +and add `check(itanium, "_Z8make_bigPiz", {"agg.result: artificial", "p: int *"});`. Expected RED: `[FAIL] ParameterDebugBinding: _Z8make_bigPiz [agg.result: int *] [p: int *]`. Commit: `test(const-param): a variadic function's hidden return argument is artificial`. + +Step 7d, the fixture's variadic constructor. The fixture becomes: + +```cpp +// SPDX-License-Identifier: Apache-2.0 +// The base-object constructor (C2) of a class with a virtual base takes a vtt after `this`: a +// parameter that the source does not declare and the debug type of the function does not list. +// ConstParameterNotModified must not report it, nor give a parameter after it the type of the +// next one, in that constructor or in the constructors that call it. + +struct Base +{ + int seed; +}; + +struct Reader : virtual Base +{ + Reader(int* x, double* y); +}; + +struct Variadic : virtual Base +{ + Variadic(int* v, double* w, ...); +}; + +// x is only read: both variants, C1 and C2, report it with its own type. +// not contains: current type: double *x +// at line 28, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'x' in function 'Reader::Reader(int*, double*)' is never used to modify the pointed object +// ↳ current type: int *x +// ↳ suggested type: const int *x +Reader::Reader(int* x, double* y) +{ + seed = *x; + *y = 0; +} + +// A variadic constructor: the `...` that ends the debug type is no parameter's. +// not contains: current type: double *v +// at line 40, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'v' in function 'Variadic::Variadic(int*, double*, ...)' is never used to modify the pointed object +// ↳ current type: int *v +// ↳ suggested type: const int *v +Variadic::Variadic(int* v, double* w, ...) +{ + seed = *v; + *w = 0; +} + +// Writer writes through p, so Leaf, which passes its own p there, must not report it. +struct Writer : virtual Base +{ + Writer(int* p, const double* q); +}; + +struct Leaf : Writer +{ + Leaf(int* p, const double* q); +}; + +Writer::Writer(int* p, const double* q) +{ + *p = static_cast(*q); +} + +// not contains: parameter 'p' in function 'Leaf::Leaf(int*, double const*)' +Leaf::Leaf(int* p, const double* q) : Writer(p, q) {} + +// not contains: parameter 'vtt' +``` + +Expected RED: the fixture also finds `current type: double *v`, in both passes. Commit: `test(const-param): report a variadic constructor's parameter with its own type`. + +- [ ] **Step 8: Take a parameter's type and its artificiality from its own declaration.** In `resolveParameterDebugBinding`, replace `if (layout.isHiddenAbiArg)` with: + +```cpp + // The declaration of the spilled parameter says whether the source declares it. + const llvm::DILocalVariable* spilled = spilledParameter(Arg); + if (layout.isHiddenAbiArg || isSyntheticDebugVariable(spilled)) +``` + +and, before the fallback on the subroutine type (`if (!binding.type && layout.hasCanonicalDebugIndex && layout.subroutineType)`): + +```cpp + // The spilled parameter's own declaration gives its type first. + if (!binding.type && spilled) + binding.type = spilled->getType(); +``` + +Expected: every check passes but `_Z8make_bigPiz`, whose hidden argument is not spilled; the fixture passes in both passes (14 of 14). Commit: `fix(const-param): take a parameter's type and artificiality from its own declaration`. + +- [ ] **Step 9: Leave the hidden return argument out of the positions.** In `buildDebugParamLayout`, before `const unsigned hiddenPrefix = irArgCount - debugParamCount;`, replace the computation of `irArgCount` and `debugParamCount` with: + +```cpp + // The hidden return argument is no source parameter, wherever the ABI puts it: first + // under the Itanium ABI, after `this` under the Microsoft one. Positions leave it out. + const llvm::Argument* hiddenReturn = nullptr; + for (const llvm::Argument& other : F.args()) + { + if (other.hasStructRetAttr()) + hiddenReturn = &other; + } + if (hiddenReturn == &Arg) + { + layout.isHiddenAbiArg = true; + return layout; + } + const unsigned irArgCount = + static_cast(F.arg_size()) - (hiddenReturn ? 1 : 0); + const unsigned argNo = + Arg.getArgNo() - + (hiddenReturn && hiddenReturn->getArgNo() < Arg.getArgNo() ? 1 : 0); + + const unsigned debugParamCount = static_cast(typeArray.size() - 1); + if (irArgCount < debugParamCount) + return layout; + +``` + +and use `argNo` instead of `Arg.getArgNo()` in the two lines that follow (`if (argNo < hiddenPrefix)`, `const unsigned debugParamOffset = argNo - hiddenPrefix;`). Expected: the 12 checks pass. Commit: `fix(const-param): leave a struct return argument out of the parameters' positions`. + +Executed history: a first Step 9 left the `...` entry out of the positional count (522344e); the independent review showed that optimized variadic functions were then misbound, so 21cfb67 reverts it, with `_Z8make_bigPiz` failing again, and 7cf76bf is this step. + +- [ ] **Step 10: Expect source indexes in the unit test.** In `describe`, after the `if`/`else` chain, add: + +```cpp + text += ", source " + + (binding.sourceIndex ? std::to_string(*binding.sourceIndex) : "none"); +``` + +Replace the expectations with: + +```cpp + check(itanium, "_Z13free_functionPiPd", {"a: int *, source 0", "b: double *, source 1"}); + check(itanium, "_ZN6Reader3sumEPiPd", + {"this: artificial, source none", "p: int *, source 0", "q: double *, source 1"}); + check(itanium, "_ZN6ReaderC1EPiPd", + {"this: artificial, source none", "x: int *, source 0", "y: double *, source 1"}); + check(itanium, "_ZN6ReaderC2EPiPd", + {"this: artificial, source none", "vtt: artificial, source none", + "x: int *, source 0", "y: double *, source 1"}); + check(itanium, "_ZNH6Picker4pickERKS_Pi", + {"self: artificial, source 0", "p: int *, source 1"}); + check(microsoft, "??0Reader@@QEAA@PEAHPEAN@Z", + {"this: artificial, source none", "x: int *, source 0", "y: double *, source 1", + "is_most_derived: artificial, source none"}); + check(blocks, "__use_block_block_invoke", + {".block_descriptor: artificial, source none", "x: int *, source 0", + "y: double *, source 1"}); + // A variadic function's debug type ends with a null entry for `...`, which no parameter + // takes; an empty parameter takes no IR argument, but keeps its debug declaration. + check(itanium, "_ZN7VarargsC2EPiPdz", + {"this: artificial, source none", "vtt: artificial, source none", + "x: int *, source 0", "y: double *, source 1"}); + check(microsoft, "??0Varargs@@QEAA@PEAHPEANZZ", + {"this: artificial, source none", "is_most_derived: artificial, source none", + "x: int *, source 0", "y: double *, source 1"}); + check(itanium, "_ZN6TaggedC2EPi3Tag", + {"this: artificial, source none", "vtt: artificial, source none", + "x: int *, source 0"}); + check(itanium, "_Z8make_bigPiz", + {"agg.result: artificial, source none", "p: int *, source 0"}); +``` + +and the comment above the test with: + +```cpp + /// resolveParameterDebugBinding: each parameter gets its own type, and its position among the + /// parameters the source declares. The others are artificial: `this`, the vtt that the + /// base-object constructor (C2) of a class with a virtual base takes, is_most_derived under the + /// Microsoft ABI, a block's descriptor. +``` + +`self` stays artificial, as on `main`: Clang marks it an object pointer, which the binding has always treated as synthetic. Its source index is 0 all the same, since the source declares it. `tag` takes no IR argument, so no check lists it. Expected RED: `error: no member named 'sourceIndex' in 'ctrace::stack::analysis::ParameterDebugBinding'`. + +- [ ] **Step 11: Give each parameter its source index.** In `include/analysis/ParameterDebugBinding.hpp`, add `#include ` after `#include `, and after `unsigned column = 0;`: + +```cpp + // The parameter's position among those the source declares, from 0. None for one the + // source does not declare (`this`, vtt, a block's descriptor), or that -O0 code does not + // spill. + std::optional sourceIndex; +``` + +The struct keeps no implicit padding: `std::optional` takes 8 bytes at a 4-byte boundary, after `column`. In `src/analysis/ParameterDebugBinding.cpp`, add `#include ` after `#include `, and after `isSyntheticDebugVariable`: + +```cpp + // The position of @p param among the parameters the source declares: the artificial ones + // before it (`this`, vtt, a block's descriptor) are not declared. An explicit object + // parameter is: Clang marks it an object pointer, not artificial. + static std::optional sourceIndexOf(const llvm::Function& F, + const llvm::DILocalVariable& param) + { + unsigned artificialBefore = 0; + for (const llvm::Argument& other : F.args()) + { + const llvm::DILocalVariable* var = spilledParameter(other); + if (var && var->getArg() < param.getArg() && var->isArtificial()) + ++artificialBefore; + } + if (artificialBefore >= param.getArg()) + return std::nullopt; + return param.getArg() - 1 - artificialBefore; + } +``` + +In `resolveParameterDebugBinding`, before `finalizeBindingName(binding, Arg);`: + +```cpp + if (spilled && !spilled->isArtificial()) + binding.sourceIndex = sourceIndexOf(F, *spilled); +``` + +Expected: the 12 checks pass with their source indexes. Commit: `feat(analysis): give a bound parameter its position among the source's`; it carries its test with its code, so that every commit builds. + +- [ ] **Step 12: Untyped declarations, once the user authorizes these cases.** A declaration that gives no type does not occur in Clang's output; the subroutine type then serves only where its entries are established, which the hand-written IR below checks. Create `test/unit/parameter_binding_unknown_input.ll`: + +```llvm +; SPDX-License-Identifier: Apache-2.0 +; A parameter's declaration gives its type; when it gives none, the debug type of the function +; serves only where its entries are the declared parameters in order: when no artificial parameter +; other than `this` exists. vtt, is_most_derived or a block's descriptor make the position +; ambiguous, since the debug type lists some of them and not others. + +; An artificial parameter other than `this`: the debug type's entries are ambiguous. x's +; declaration gives its type, double *, which is kept. +define void @ambiguous_typed(ptr %hidden, ptr %x) !dbg !20 { +entry: + %hidden.addr = alloca ptr + %x.addr = alloca ptr + store ptr %hidden, ptr %hidden.addr + #dbg_declare(ptr %hidden.addr, !21, !DIExpression(), !23) + store ptr %x, ptr %x.addr + #dbg_declare(ptr %x.addr, !22, !DIExpression(), !23) + ret void, !dbg !23 +} + +; The same, but x's declaration gives no type: x stays without one. +define void @ambiguous_untyped(ptr %hidden, ptr %x) !dbg !30 { +entry: + %hidden.addr = alloca ptr + %x.addr = alloca ptr + store ptr %hidden, ptr %hidden.addr + #dbg_declare(ptr %hidden.addr, !31, !DIExpression(), !33) + store ptr %x, ptr %x.addr + #dbg_declare(ptr %x.addr, !32, !DIExpression(), !33) + ret void, !dbg !33 +} + +; No artificial parameter: the debug type's entries are the parameters, in order. x's declaration +; gives no type, the debug type gives double *. +define void @established_untyped(ptr %x) !dbg !40 { +entry: + %x.addr = alloca ptr + store ptr %x, ptr %x.addr + #dbg_declare(ptr %x.addr, !41, !DIExpression(), !42) + ret void, !dbg !42 +} + +!llvm.dbg.cu = !{!0} +!llvm.module.flags = !{!2, !3} + +!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "hand-written", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug) +!1 = !DIFile(filename: "parameter_binding_unknown_input.ll", directory: ".") +!2 = !{i32 7, !"Dwarf Version", i32 5} +!3 = !{i32 2, !"Debug Info Version", i32 3} +!10 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed) +!11 = !DIBasicType(name: "double", size: 64, encoding: DW_ATE_float) +!12 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !10, size: 64) +!13 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !11, size: 64) +!14 = !DISubroutineType(types: !15) +!15 = !{null, !12, !12, !12} +!16 = !DISubroutineType(types: !17) +!17 = !{null, !13} +!20 = distinct !DISubprogram(name: "ambiguous_typed", scope: !1, file: !1, line: 1, type: !14, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0) +!21 = !DILocalVariable(name: "hidden", arg: 1, scope: !20, file: !1, line: 1, type: !12, flags: DIFlagArtificial) +!22 = !DILocalVariable(name: "x", arg: 2, scope: !20, file: !1, line: 1, type: !13) +!23 = !DILocation(line: 1, scope: !20) +!30 = distinct !DISubprogram(name: "ambiguous_untyped", scope: !1, file: !1, line: 2, type: !14, scopeLine: 2, spFlags: DISPFlagDefinition, unit: !0) +!31 = !DILocalVariable(name: "hidden", arg: 1, scope: !30, file: !1, line: 2, type: !12, flags: DIFlagArtificial) +!32 = !DILocalVariable(name: "x", arg: 2, scope: !30, file: !1, line: 2) +!33 = !DILocation(line: 2, scope: !30) +!40 = distinct !DISubprogram(name: "established_untyped", scope: !1, file: !1, line: 3, type: !16, scopeLine: 3, spFlags: DISPFlagDefinition, unit: !0) +!41 = !DILocalVariable(name: "x", arg: 1, scope: !40, file: !1, line: 3) +!42 = !DILocation(line: 3, scope: !40) +``` + +Declare `LoadedModule unestablished;` after `LoadedModule blocks;`, load it last: + +```cpp + !load("parameter_binding_block_input.c", {"-fblocks"}, blocks) || + !load("parameter_binding_unknown_input.ll", {}, unestablished)) + return false; +``` + +and add, after the last check of Step 10: + +```cpp + // A declaration's type is kept; without one, the debug type serves only where its + // entries are established. + check(unestablished, "ambiguous_typed", + {"hidden: artificial, source none", "x: double *, source 0"}); + check(unestablished, "ambiguous_untyped", + {"hidden: artificial, source none", "x: no type, source 0"}); + check(unestablished, "established_untyped", {"x: double *, source 0"}); + +``` + +Expected RED: `[FAIL] ParameterDebugBinding: ambiguous_untyped [hidden: artificial, source none] [x: int *, source 0]`; `ambiguous_typed` passes since Step 8, `established_untyped` keeps `main`'s fallback. Commit: `test(const-param): an untyped parameter takes the debug type only where established`. + +- [ ] **Step 13: Use the debug type of the function only where its entries are established.** Add `#include ` before `#include `, and before `buildDebugParamLayout`: + +```cpp + // Whether F has an artificial parameter other than `this`: a vtt, an is_most_derived or a + // block's descriptor. The subroutine type lists the descriptor, not what the C++ ABI adds. + static bool hasArtificialOtherThanThis(const llvm::Function& F) + { + return llvm::any_of(F.args(), + [](const llvm::Argument& other) + { + const llvm::DILocalVariable* var = spilledParameter(other); + return var && var->isArtificial() && !var->isObjectPointer(); + }); + } + +``` + +In `buildDebugParamLayout`, replace the spilled branch with: + +```cpp + // The spill slot names the source parameter whatever the ABI did to the list; the + // position below only serves arguments that are not spilled, like a hidden sret. + if (const llvm::DILocalVariable* spilled = spilledParameter(Arg); + spilled && spilled->getArg() != 0) + { + // Its declaration gives its type. Without one, the subroutine type's entry at its + // debug number is its own only when no parameter makes that position ambiguous. + if (spilled->getType() || + (spilled->getArg() <= listed && !hasArtificialOtherThanThis(F))) + { + layout.debugArgIndex = spilled->getArg(); + layout.hasCanonicalDebugIndex = true; + } + return layout; + } + +``` + +Expected: the 15 checks pass. Mutants checked on the prototype: without the fallback for an untyped declaration, `established_untyped` fails; with a fallback even where the position is ambiguous, `ambiguous_untyped` fails. Commit: `fix(const-param): use the debug type only where its entries are established`. + +- [ ] **Step 14: Cases from the independent review, once the user authorizes them; one commit each, subjects of at most 84 characters (`scripts/ci/commit_checker.py`).** In `test/unit/parameter_binding_input.cpp`, a method of `Maker` returning `Big` (under the Microsoft ABI its hidden return argument comes after `this`) and `TagFirst(Tag tag, int* x)` (an empty first parameter); `test/unit/parameter_binding_o2_input.ll`, generated by Clang 20 at `-O2` for x86-64 Linux from a variadic constructor of a class with a virtual base and a variadic function taking a struct by value; `describe` names a type that is not a pointer; checks: `_ZN8TagFirstC2E3TagPi` (`x: int *, source 1`), `_ZN8VariadicC2EPiPdz` and `split_variadic` at `-O2` (each argument bound to its own declaration), and the Microsoft hidden return argument without a type. A fixture, `implicit-move-constructor.cpp`: an implicitly defined move constructor's parameter is not reported. Code and failure evidence: `evidence/217/p0/proposals/` and `harness/test_G.inc`. + +- [ ] **Step 15: Format, full suite, warnings.** Run clang-format on the changed C++ files, the build (no warning: `-Wpadded` is on), both unit test executables, and the suite. Expected: everything passes, no expectation changed; a formatting change, if any, goes in its own commit. + +**Validation of P0.1:** +- `testParameterDebugBinding`: every check passes (12 before Step 12, 15 after Step 13, 19 after Step 14); both unit test executables pass. +- `virtual-base-vtt.cpp` passes in the default and smt-z3 passes (14 of 14). +- The suite passes on macOS with no expectation changed; no new warning; clang-format clean. + +### Task P0.2: measure the effect without the option, then deliver + +Expected effect on the rule, without the option: +- base-object constructors (C2, and CI2 for inherited ones) of a class with a virtual base: no suggestion on `vtt`; the parameters after it are reported with their own type, so a suggestion that the shifted type hid can appear, and one it caused disappears; +- constructors that call such a C2: the #167 contract reads the callee's real parameter types; a false "passed where a pointer to const is expected" disappears, and a true one can appear; +- Microsoft ABI targets: no suggestion on `is_most_derived`; +- blocks (`-fblocks`, the default on macOS): no suggestion on `.block_descriptor`; +- variadic functions: the constructors above keep their types when variadic, and a hidden return argument is artificial wherever the ABI puts it; under the Microsoft ABI, the one after `this` no longer carries `this`'s const contract; +- parameters that Clang marks artificial are no longer reported: the parameter of an implicitly defined special member (an implicit move constructor's), the hidden argument that `pass_object_size` adds, the `__context` of a captured statement; +- a parameter whose declaration gives no type, in a function with an artificial parameter other than `this`, gets no type instead of a guessed one, so the rule does not report it (Clang always gives the type); +- nothing else: line, column, confidence and name of every other parameter unchanged; the other rules use `spilledParameter` and `declaredVariable`, which do not change. + +- [ ] **Step 1: Compare every fixture's output.** + +```bash +D=/Users/hugopayet/Desktop/CLaude/coretrace-stack-analyzer-evidence/217/p0/effect +python3 $D/diff_fixture_outputs.py
$D/fixtures-branch +``` + +Expected: `changed: 0` among `main`'s fixtures (the script normalizes the logger's process id). The prototype gave `fixtures: 424, changed: 0`. + +- [ ] **Step 2: Linux (WSL), suite and corpora.** Build `main` and the branch; run the suite; run both binaries without the option on the self-analysis as CI does (4 chunks, 2 at a time, fast profile, cold caches), zlib `767c4c9`, tinyxml2 `8224e42` and PCRE2 `3e4036e` (without the #215 units). Expected: identical outputs; none of these corpora compiles a class with a virtual base or a block (zlib's `contrib/iostream` is not in its compilation database). The self-analysis time is reported as an indication. + +- [ ] **Step 3: Independent review.** A fresh reviewer on the most capable model reviews the P0 branch against this plan and the spec (`review-package`, then `requesting-code-review`'s `code-reviewer.md`); its findings are sorted and fixed as superpowers:executing-plans says, each fix with a test that failed first, and reported to the user before the merge. + +- [ ] **Step 4: Pull request.** After the user's go: push, pull request assigned to `SizzleUnrlsd`, label `bug`, `Fixes #`, body prepared in `evidence/217/p0/pr-body.md` (root cause, effect without the option, measurements). CI; code scanning triaged as `CLAUDE.md` describes, summary in the body. The user merges. + +**Validation of P0.2:** no existing fixture output changes; corpora identical without the option; independent review done and its findings handled; CI green; triage summarized; the effect stated in the body matches the measurements. + +### Task I.1: integrate `main` into `217-t-v2`, once P0 is merged + +- [ ] **Step 1:** In `wt-217-v2`, `git fetch origin && git merge origin/main`. Conflicts expected in `src/analysis/ConstParamAnalysis.cpp`: keep #221's `issue.debugArgNo` until Task 1.2 removes it. +- [ ] **Step 2:** Build, both unit test executables, suite (T fixtures carry `// analyzer-arg: --experimental-const-suggestion-check`), on macOS and Linux. +- [ ] **Step 3:** Replay `evidence/217/final-review/repro.sh`; record which review cases change because of P0 (expected: `t10` loses its `vtt` suggestion). +- [ ] **Step 4:** Commit the merge (`Merge origin/main into 217-t-v2`). + +**Validation of I.1:** every test of #221 passes; only the differences that P0 explains in the review cases. + +--- + +## Stage 1 — flow, identities, plan and scope, facts, correspondences, collisions, trace (§9.3) + +Catalogue: D1 (T part), D2, D3, D4, D5, D7, D11, D14, D15, D16, D17. + +**Detail, written at the stage's start** (Task I.1 validated at `64ed37e`). Each new or changed test is written out in full with its failure at `64ed37e`. They were run from `evidence/217/stage1/tests/`: the proposed `run_test.py` (`patched/`), the fixtures (`fixtures/new/`, `fixtures/changed/`), the runner `run_red.py` and its output `red-64ed37e.txt`. A test enters the repository only once the user authorizes it; its task starts by running it again to watch it fail. For the code, this section gives the interfaces, the decision logic and every new text; T v1's code that only moves keeps its form and is named. + +Commands used by the steps, from the worktree root: +- one check: `python3 -c 'import sys, run_test as r; r.RUN_CONFIG.cache_enabled = False; sys.exit(not r.())'`; +- one fixture: `python3 -c 'import sys, run_test as r; from pathlib import Path; r.RUN_CONFIG.cache_enabled = False; ok, total, passed, report = r.check_file(Path("test/pointer_reference-const_correctness/")); print(report); sys.exit(not ok)'`; +- build, unit tests, suite and format: Global Constraints. + +**D17 (confirmed by the user).** §5.10 makes a stale declaration indeterminate. T v1 removes that suggestion, and two checks pin it: `check_const_suggestion_cross_tu` (`("def and a redeclaration", ["def.c", "redecl.c"], [])`) and `check_const_suggestion_compilation_database` (`("a redeclaration, not analyzed", ["def.c", "redecl.c"], "read_value", None)`). Both expectations change in Task 1.6, submitted with their failure evidence; the spec's catalogue and §7 list D17. Indeterminate keeps the suggestion as not checked: it does not show that the suggestion is valid, and the reason says so through the file and the link-edition assumption. + +### Texts + +Every new text, verbatim. `` is the name the diagnostic shows, `` a symbol, `` a real path, `''` a demangled name. + +Reasons of an indeterminate verdict, after `not checked with Clang: ` (shown in the diagnostic): + +| Reason | Task | When | +|---|---|---| +| `the parameter's type is written in a macro argument that writes other parameters too` | 1.3 | D5: the text an edit rewrites writes another parameter | +| `the rewritten code cannot be inspected entirely, a fatal error stops its compilation: :: ` | 1.4 | step 2 | +| `the rewrite also changes the type of (:)` | 1.4 | step 3 | +| `the declarations at : cannot be compared one to one: before the rewrite, after it` | 1.4 | step 3: a site of a rewritten file, outside the plan, whose declarations differ in number before and after the rewrite, a site on one side only included | +| `the facts at : cannot be compared one to one: before the rewrite, after it` | 1.4 | step 6: a key whose facts differ in number before and after the rewrite, a fact on one side only included; in Task 1.4, once T v1's comparison of the code, which refutes first, finds no difference | +| `the generated code changes, and no reference, override or constant condition explains it: ` | 1.5 | step 7 (D16); `` is the comparator's text | +| `'' and '' both become '': their correspondence is not one to one` | 1.5 | §5.7, D14 | +| `'' becomes both '' and '': their correspondence is not one to one` | 1.5 | §5.7 | +| ` is also defined in ; if and are linked together, the two definitions collide` | 1.6 | §5.10, strong (D4) | +| ` is also defined in ; if and are linked together, the strong definition, in , is used and the weak one, in , is ignored` | 1.6 | §5.10, one definition strong and the other weak, whichever is the entity's | +| ` is also defined in ; if and are linked together, the linker keeps one of the two weak definitions, and the check establishes neither which one nor that they are equivalent` | 1.6 | §5.10, both weak (`weak`, `weak_odr`, `linkonce`, `linkonce_odr`) | +| `the rewrite gives the symbol of another declaration (:); if a file linked with defines it, the two definitions collide` | 1.6 | ruling 3 | +| ` declares with its current type and does not see the rewrite: if and are linked together, that declaration no longer matches the rewritten definition` | 1.6 | D17 | +| `: converts the address of with an explicit cast` | 1.7 | D15 | + +Reasons of a refuted verdict, in the trace only (a refuted suggestion is removed), after T v1's `removed, `: + +| Reason | Task | +|---|---| +| `: refers to '' instead of ''` | 1.4, R | +| `'' overrides instead of ` (`'', ''`, or `nothing`) | 1.4, O | +| `the condition at : becomes ` | 1.4, K (`if constexpr`) | +| `the _Generic selection at : chooses another association` | 1.4, K | + +T v1's texts stay, `does not compile: :: …` and `gains a checked warning: :: …` included. + +Trace (`--verbose`). T v1's line `[const-suggestion] parameter : ` comes first, unchanged. The new lines follow it; none matches T v1's pattern `(\S+) parameter (\S+): `, which `check_const_suggestion_removal_reason` reads: + +``` +[const-suggestion] parameter , suggestion: on , source index +[const-suggestion] parameter , transformation: becomes () +[const-suggestion] parameter , rewritten at: : +[const-suggestion] parameter , unit []: [: ] +[const-suggestion] parameter , verdict: [: ] +``` +- one `rewritten at` line per edit, none when the plan is not executed; one `unit` line per unit judged, in judgement order (Task 1.1 adds these lines, Task 1.8 the others); +- ``: `verified`, `refuted` or `indeterminate`; +- ``, from §3: `pointer to const`, `pointer to const, top-level const kept`, `reference to const`, `reference to const; passing by value is not checked`; +- ``: 16 lowercase hexadecimal digits, `llvm::xxh3_64bits` of the unit's arguments and working directory as T builds them, each followed by a NUL byte. + +### Rulings + +The spec leaves these open. The user confirmed them on 2026-10-17, ruling 2 provisionally, and corrected rulings 7 and 12; rulings 14 and 15 come from that review. +1. **Which reason is shown.** A refuted suggestion gives the reason of the first unit that refutes it, in judgement order: the defining unit, then the others in T v1's order. An indeterminate one gives the reason of the first indeterminate unit in that order; if no unit is indeterminate, the first collision's, a definition before a declaration. Cost if wrong: another reason is shown; the verdict stays. +2. **The initial compilation of another unit is shared by the suggestions of one module.** T runs once per analyzed module (`checkConstSuggestions`): a unit that sees the changes of two modules is compiled once as it is for each. Sharing it across modules needs the facts of entities not planned yet. The user accepted this limit for stage 1, provisionally; the spec states it (§5, step 3; §10): it reduces the sharing planned at first. Task 3.1 measures its cost across modules; the +10 % criterion is unchanged. +3. **A declaration of another entity that the rewrite merges into the entity** (t9 without `b.cpp`: `api.hpp` also declares `f(const int*)`) is a possible collision (§3, "collision possible entre fichiers"): indeterminate, with its own reason, shown after a definition's. Cost if wrong: a suggestion kept as not checked that a definition elsewhere would refute or not. +4. **R identifies a target by its declarations.** The value of an R fact is its target's declaration sites, mapped back to the original text; for a template specialization or an implicit function, which have no declaration of their own, its symbol, mapped back through the established pairs. An R fact is unchanged when its target after the rewrite keeps every declaration it had: a merge (ruling 3) adds the entity's, and the collisions judge it, not the facts. A target that loses a declaration, or another target, refutes. A symbol alone would not do: once t9 is rewritten, `_Z1fPKi` names both the entity and the overload it merges with. Cost if wrong: a merge refuted, or a reference to the wrong declaration missed. +5. **D3 is pinned in Task 1.4, not 1.6**: both directions are fixed by the source identity with its linkage, which Task 1.4 brings. Task 1.6 keeps D4, Review Focus 3 and D17. +6. **Task 1.1 gets a second test**, the shared initial compilation, counted with `--timing`: the plan named it as work but did not pin it. +7. **D7's fixture holds five cases**: t8, and as templates a base class template, a derived class template and a constructor template, inherited for `int*` and for `double*`. The technique of §5.7 holds on all of them in `evidence/217/stage1/d7-probe/` (`probe.cpp`, `probe-output.txt`) and `evidence/217/stage1/corrections/` (`generic-before.cpp`): `getInheritedConstructor()` finds the constructor each inheriting constructor comes from, and the symbols CodeGen gives the inheriting constructors are the IR's, before and after. So D7 expects verified. An inheriting constructor is paired by its derived class, its variant, and the specialization it inherits: one derived class inherits a constructor template once per specialization (`FromGeneric` for `int*` and `double*`), which the derived class and the variant alone confuse. +8. **The two reasons of Task 1.2 are not added.** After P0, the rule reports no artificial parameter, and every parameter of the `-O0` code the analyzer compiles from sources is spilled, so it has a source index: no input reaches those reasons, and no test could fail first. A binding without a source index, or artificial, keeps T v1's text `the parameter is not found in the source`. Cost if wrong: a less precise reason on an input not found. +9. **The comparator's name heuristics are removed** once the established pairs replace them (Task 1.5): the masked encodings, `withoutConst`, `variantOf` and the template keys. The difference text keeps listing the symbols left without a pair, which locates the difference, as §5.9 asks. Cost if wrong: a trace that no longer suggests a probable counterpart. +10. **D12 (Task 2.4) also changes the `ReferenceRvaluePreferValue` block that D14 adds** to `const-suggestion-deduction-refused.cpp`. +11. **Task 1.4 gets a check of the scope after the rewrite** (`check_const_suggestion_header_read_differently`): a header that another file reads as other declarations, through a macro it defines otherwise. Without it, step 3 of §5.8 would have no failing test: the plan's scope (Task 1.3) holds in the defining unit. +12. **No error limit; step 2 tested.** T's compilations set no error limit: otherwise a rewrite that breaks more than 19 uses would end in "too many errors", which is fatal, and step 2 would keep as not checked a suggestion that T v1 removes. A fatal error still comes from the rewrite itself when the suggested type selects a template whose instantiation never ends ("recursive template instantiation exceeded maximum depth"): `const-suggestion-incomplete-inspection.cpp` tests step 2 with it. +13. **Task 1.4 gets a fixture where R and K refute without a change of code** (`const-suggestion-facts-without-code-change.cpp`). T v1 compares only code and keeps both suggestions as checked; since every refutation of T v1's tests comes with a code difference, the facts of Task 1.4 would otherwise have no failing test. +14. **A declaration is known by where it is expanded and where it is written.** One macro invocation can declare several functions, whose names it expands at the same place: a `DeclarationSite` holds both positions, for the identity of the entity and for the scope after the rewrite. `const-suggestion-macro-two-functions.c` pins it: it passes at `64ed37e`, where T v1 finds a function by its symbol, and its failure is shown on a mutant of Task 1.4 that keeps only the expansion position. +15. **The comparator's unit tests are adapted in Task 1.5**, as changed tests submitted with the task: `testConstSuggestionCodeComparison` and `testConstSuggestionNormalizations` pass the established pairs, and keep their guarantees; `testConstSuggestionWithoutConstKeepsIdentifiers` goes with `withoutConst`, its guarantee (two functions whose names differ by `const` are not paired) now held by a case where a renamed symbol without a pair is a difference. +16. **At planning, the scope of D5 covers the parameters only.** Another declaration whose type a macro argument shares with the parameter, such as a variable that the same invocation declares, is caught after the rewrite by the scope of Task 1.4 (step 3, `the rewrite also changes the type of (:)`), which `check_const_suggestion_header_read_differently` pins; at planning, no test would reach it. Cost if wrong: that case gets the step-3 reason. +17. **Only a refutation stops the judgement of the units** (§5.10: refuted if one unit refutes; otherwise indeterminate if one unit or one collision is). T v1 also stopped once the defining unit was not verified; after Task 1.7, a cast in the defining file made it indeterminate, and the error of another file that the rewrite breaks went unseen (`check_const_suggestion_cast_in_the_defining_file`). The user confirmed on 2026-10-21 that every unit that sees the change is judged until one refutes, whatever the defining unit's verdict (`a60a1b4`). Cost: when the defining unit is not verified, the other units are compiled too; a verdict can only move from indeterminate to refuted. + +### Shared types + +In `ConstSuggestionInternal.hpp`, each added by the task that first needs it: +```cpp +enum class Verdict : std::uint8_t { Verified, Refuted, Indeterminate }; // 1.1 +struct Judgement { Verdict verdict = Verdict::Verified; std::string reason; }; // 1.1 +struct UnitConfiguration { std::string file; UnitCommand command; std::string fingerprint; }; // 1.1 +struct SourcePosition // 1.4 +{ + std::string file; // real path + unsigned offset = 0; // in the original text + auto operator<=>(const SourcePosition&) const = default; +}; +struct DeclarationSite // 1.4 +{ + SourcePosition expansion; // where Clang expands the name + SourcePosition spelling; // where it is written (ruling 14) + auto operator<=>(const DeclarationSite&) const = default; +}; +enum class SourceLinkage : std::uint8_t { None, Internal, External }; // 1.4 +struct EntityIdentity // 1.4 +{ + std::vector redeclarations; // the defining unit's, sorted + SourceLinkage linkage = SourceLinkage::None; + std::optional templateOrigin; // the pattern's first declaration + std::vector symbols; // every variant's, before the rewrite (CodeGen) +}; +struct Candidate { std::string current; std::string type; std::string alternative; }; // 1.8 +struct SuggestionPlan // 1.3 +{ + EntityIdentity entity; // 1.4 + unsigned sourceIndex = 0; + Candidate candidate; // 1.8 + std::string expectedType; // the candidate, written canonically (T v1's expectedType) + std::map> edits; // by real path + std::optional indeterminate; // why the plan is not executed +}; +enum class FactKind : std::uint8_t { Reference, Override, Constant }; // 1.4 +struct FactKey // 1.4 +{ + std::vector contexts; // the instantiations around the fact, outermost first + DeclarationSite site; // the expression's, both positions + auto operator<=>(const FactKey&) const = default; +}; +struct Fact { FactKind kind; FactKey key; std::vector value; }; // 1.4 +struct SymbolPair { std::string before; std::string after; }; // 1.5, in ConstSuggestionCheck.hpp +``` +`FactKey` has no unit: each unit's facts are kept apart. `Fact::value` is sorted: for R, the target's declaration sites, or its symbol (ruling 4); for O, the overridden methods, each the same way; for K, `true`, `false` or the selected association's index. + +### Task 1.1: units, verdicts, aggregation, report + +**Files:** `src/analysis/ConstSuggestionCheck.cpp`, `src/analysis/ConstSuggestionInternal.hpp`, `run_test.py`. + +**Interfaces.** Produces `Verdict`, `Judgement`, `UnitConfiguration` (Shared types) and: +```cpp +// §5.10: refuted if a judgement refutes, else indeterminate if one is, else verified; with the +// reason of the first judgement that has that verdict (ruling 1). +Judgement aggregate(const std::vector& judgements); +// 16 hexadecimal digits: xxh3 of the arguments and the working directory, each NUL-terminated. +std::string fingerprintOf(const UnitCommand& command); +``` + +**Tests.** A helper that later tasks' checks share, and two checks, registered after `check_const_suggestion_both_modes`: + +```python +def _const_suggestion_run( + args: list[str], env_overrides: Optional[dict[str, str]] = None +) -> Optional[tuple[dict[str, list[str]], list[str], str]]: + """ + #217: runs the analyzer with the Clang check of const suggestions and its trace (--verbose), on + inputs the cache key does not cover. Returns the messages of the const suggestions by function + (a function reported twice, such as a constructor's two variants, has two), the trace's lines + without their "[const-suggestion] " prefix, and the whole output; None when the analysis did + not run. + """ + result = run_analyzer_uncached( + [*args, "--format=json", "--experimental-const-suggestion-check", "--verbose"], + env_overrides, + ) + if result.returncode != 0: + return None + try: + diagnostics = json.loads(result.stdout or "").get("diagnostics", []) + except json.JSONDecodeError: + return None + messages: dict[str, list[str]] = {} + for d in diagnostics: + if str(d.get("ruleId", "")).startswith("ConstParameterNotModified"): + function = str(d.get("location", {}).get("function", "")) + messages.setdefault(function, []).append(str(d.get("details", {}).get("message", ""))) + output = (result.stdout or "") + (result.stderr or "") + trace = [ + line.split("[const-suggestion] ", 1)[1] + for line in output.splitlines() + if "[const-suggestion] " in line + ] + return messages, trace, output +``` + +Review Focus 4, the one configuration of a file the database lists twice: + +```python +def check_const_suggestion_one_configuration_per_file() -> bool: + """ + #217: a compilation database can list a file twice, with different commands. The analysis + loads one command per file, and the Clang check of const suggestions compiles each file with + that one only: its trace names one configuration for each file, here the defining file, + analyzed, and a caller that only the database lists. + """ + print("=== Testing const suggestions with files the compilation database lists twice ===") + fixtures = (RUN_CONFIG.test_dir / "pointer_reference-const_correctness").resolve() + definition = fixtures / "cross-tu-const-suggestion-def.c" + caller = fixtures / "cross-tu-const-suggestion-caller.c" + unit = re.compile(r"read_value parameter p, unit (\S+) \[([0-9a-f]+)\]: ") + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_twice_") as tmp: + database = Path(tmp).resolve() / "compile_commands.json" + # A macro the files do not use: the two commands differ, the code does not. + database.write_text( + json.dumps( + [ + { + "directory": str(fixtures), + "file": str(f), + "arguments": ["clang", *extra, "-I", str(fixtures), "-c", str(f)], + } + for f in (definition, caller) + for extra in ([], ["-DLISTED_TWICE"]) + ] + ) + ) + run = _const_suggestion_run([str(definition), f"--compile-commands={database}"]) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + configurations: dict[str, set[str]] = {} + for line in trace: + match = unit.match(line) + if match: + configurations.setdefault(match.group(1), set()).add(match.group(2)) + checked = "checked in every file of the compilation database that sees the change" + ok = True + for label, good in ( + ( + "the suggestion is checked in the compilation database", + any(checked in text for text in messages.get("read_value", [])), + ), + ( + "one configuration of the defining file", + len(configurations.get(str(definition), ())) == 1, + ), + ("one configuration of the caller", len(configurations.get(str(caller), ())) == 1), + ): + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {configurations}")) + ok = ok and good + print() + return ok +``` + +Ruling 6, the initial compilation shared by the suggestions: + +```python +def check_const_suggestion_compiles_each_file_once() -> bool: + """ + #217: the Clang check compiles each file that sees a change once as it is, for all the + suggestions it checks there: here two functions, defined in one file and called in another. + Each file is compiled once before the rewrites (`--timing`), and both suggestions are checked. + """ + print("=== Testing that each file is compiled once as it is for several const suggestions ===") + sources = { + "readers.h": "int read_a(int* p);\nint read_b(int* q);\n", + "readers.c": '#include "readers.h"\n' + "int read_a(int* p)\n{\n return *p;\n}\n" + "int read_b(int* q)\n{\n return *q + 1;\n}\n", + "user.c": '#include "readers.h"\n' + "int use_both(void)\n{\n int v = 1;\n return read_a(&v) + read_b(&v);\n}\n", + } + calls = re.compile(r"const_suggestion\.compile\.(\w+) .*\bcalls=(\d+)") + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_once_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + # Every hotspot, not the 20 longest. + run = _const_suggestion_run( + [str(root / "readers.c"), str(root / "user.c"), "--timing"], + {"CTRACE_HOTSPOT_TOP": "200"}, + ) + if run is None: + return fail_check("the analysis did not run") + messages, _, output = run + compiled = {m.group(1): int(m.group(2)) for m in calls.finditer(output)} + checked = "checked in the analyzed files only" + ok = True + for label, good in ( + ( + "both suggestions are checked", + all(any(checked in t for t in messages.get(f, [])) for f in ("read_a", "read_b")), + ), + ("the defining file is compiled once as it is", compiled.get("defining_before") == 1), + ("the other file is compiled once as it is", compiled.get("other_before") == 1), + ): + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {compiled}")) + ok = ok and good + print() + return ok +``` + +Failure at `64ed37e`: no trace line names a unit; the caller is compiled once per function name. + +```text +check_const_suggestion_one_configuration_per_file: FAIL +=== Testing const suggestions with files the compilation database lists twice === + ✅ the suggestion is checked in the compilation database + ❌ one configuration of the defining file: {} + ❌ one configuration of the caller: {} +``` + +```text +check_const_suggestion_compiles_each_file_once: FAIL +=== Testing that each file is compiled once as it is for several const suggestions === + ✅ both suggestions are checked + ✅ the defining file is compiled once as it is + ❌ the other file is compiled once as it is: {'after': 4, 'other_before': 2, 'defining_before': 1} +``` + +- [ ] **Step 1: Add the helper and the two checks, then run them.** + Run: the one-check command for `check_const_suggestion_one_configuration_per_file`, then for `check_const_suggestion_compiles_each_file_once`. + Expected: FAIL, as above. +- [ ] **Step 2: Commit the tests.** + `test(const-param): name the one configuration of a file listed twice`; + `test(const-param): compile each file once as it is for all its suggestions`. +- [ ] **Step 3: Judgements and aggregation.** Add `Verdict`, `Judgement`, `UnitConfiguration` and `aggregate` to `ConstSuggestionInternal.hpp` and `ConstSuggestionCheck.cpp`: + ```cpp + Judgement aggregate(const std::vector& judgements) + { + for (const Verdict verdict : {Verdict::Refuted, Verdict::Indeterminate}) + { + for (const Judgement& judgement : judgements) + { + if (judgement.verdict == verdict) + return judgement; + } + } + return {}; + } + ``` + `checkUnit` returns a `Judgement` instead of its `bool` and its `removed` and `notChecked` strings: T v1's `removed` text is a refutation's reason, its `notChecked` text an indeterminate one's, and a unit that passes is verified. The loop over the units keeps T v1's order and stops at the first refutation only (ruling 17); T v1 also stopped once the defining unit was not verified. The outcome maps onto T v1's report: refuted removes the suggestion (`removed, `), indeterminate adds `kNotChecked + reason` (`not checked, `), verified adds the "checked" line and the ABI line, unchanged. + Commit: `refactor(const-param): judge each unit and aggregate three verdicts`. +- [ ] **Step 4: Units and their configuration.** A unit is a `UnitConfiguration`: the file's real path (`canonicalPath`), its `UnitCommand` (`commandFor`, cached as T v1 caches it in `AnalyzedUnits::commands`), and `fingerprintOf(command)`: + ```cpp + std::string fingerprintOf(const UnitCommand& command) + { + std::string text; + for (const std::string& arg : command.args) + text.append(arg).push_back('\0'); + text.append(command.workingDir).push_back('\0'); + return llvm::utohexstr(llvm::xxh3_64bits(text), /*LowerCase=*/true, /*Width=*/16); + } + ``` + After each unit's judgement, the trace gives a line ` parameter , unit []: [: ]`, through a second `coretrace::log` call next to T v1's `trace`. + Run: the one-check command for `check_const_suggestion_one_configuration_per_file`. Expected: PASS. + Commit: `feat(const-param): name each unit and its configuration in the trace`. +- [ ] **Step 5: One initial compilation per unit and configuration.** `originalOf` is keyed by the unit (`file` and `fingerprint`), no longer by `(unit, function name)`, and compiles the unit once with every target of the module: `Original` keeps one entry of facts per target (`std::vector`, indexed like `targets`). `checkUnit` reads the entry of its suggestion. + Run: the one-check command for `check_const_suggestion_compiles_each_file_once`. Expected: PASS. + Commit: `perf(const-param): compile each other file once as it is for all suggestions`. +- [ ] **Step 6: Task tests.** Build; unit tests; the T v1 checks and fixtures (`python3 run_test.py --jobs 8 --no-cache`). Expected: all pass; the trace lines of T v1 unchanged (`check_const_suggestion_removal_reason`). + +**Validation:** every T v1 test passes unchanged; both new checks pass. + +### Task 1.2: the parameter from P0 + +**Files:** `src/analysis/ConstSuggestionCheck.cpp`, `include/analysis/ConstParamAnalysis.hpp`, `src/analysis/ConstParamAnalysis.cpp`, `test/pointer_reference-const_correctness/const-suggestion-virtual-base.cpp` (new). + +**Interfaces.** Consumes `ParameterDebugBinding::sourceIndex` and `isArtificial` (P0). Removes `ConstParamIssue::debugArgNo` (#221) and `Target::debugArgNo`. + +**Tests.** D1, a new fixture (t10). `x` is reported twice, in C2 then C1; the ABI line tells them apart: + +`test/pointer_reference-const_correctness/const-suggestion-virtual-base.cpp`: + +```cpp +// SPDX-License-Identifier: Apache-2.0 +// #217: the base-object constructor (C2) of a class with a virtual base takes a hidden `vtt` after +// `this`. The check rewrites the parameter the suggestion names, found by its index among the +// parameters the source declares: `x`, in both variants. `y`, which the constructor writes +// through, is not rewritten. + +// strict-expectation-details: true + +struct V +{ + int v = 0; +}; + +struct D : virtual V +{ + int a, b; + D(int* x, int* y); +}; + +// at line 33, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'x' in function 'D::D(int*, int*)' is never used to modify the pointed object +// ↳ current type: int *x +// ↳ suggested type: const int *x +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_ZN1DC2EPiS0_ -> _ZN1DC2EPKiPi): code compiled separately must use the updated declaration and be recompiled + +// at line 33, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'x' in function 'D::D(int*, int*)' is never used to modify the pointed object +// ↳ current type: int *x +// ↳ suggested type: const int *x +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_ZN1DC1EPiS0_ -> _ZN1DC1EPKiPi): code compiled separately must use the updated declaration and be recompiled +D::D(int* x, int* y) : a(*x), b(0) +{ + *y = 1; +} + +int make() +{ + int p = 1, q = 2; + D d(&p, &q); + return d.a + q; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check +``` + +Failure at `64ed37e`: C1's block is found, C2's is missing: T rewrites `y` for the suggestion on `x` in C2, and removes it (`read-only variable is not assignable`). + +```text +new/const-suggestion-virtual-base.cpp: FAIL (4/6) +=== Testing evidence/217/stage1/tests/fixtures/new/const-suggestion-virtual-base.cpp === + ❌ (default) expectation #1 MISSING + ✅ (default) expectation #2 FOUND + ❌ (smt-z3) expectation #1 MISSING + ✅ (smt-z3) expectation #2 FOUND +``` + +Review Focus 2: `const-suggestion-explicit-object.cpp` unchanged, passing at `64ed37e`. + +- [ ] **Step 1: Add the fixture and run it.** Run: the one-fixture command for `const-suggestion-virtual-base.cpp`. Expected: FAIL, expectation #1 (C2) missing. + Commit: `test(const-param): check the parameter a C2 constructor's suggestion names`. +- [ ] **Step 2: The source index.** In `Inspector::plan`, the parameter is P0's: + ```cpp + const ParameterDebugBinding& binding = target.binding; + if (binding.isArtificial || !binding.sourceIndex) + { + facts.notRewritten = "the parameter is not found in the source"; + return; + } + facts.paramIndex = *binding.sourceIndex; + ``` + `Target` carries the issue's `binding` instead of `debugArgNo`; the `implicitThis` arithmetic goes. Ruling 8: no new text. + Run: the one-fixture command. Expected: PASS. Then `const-suggestion-explicit-object.cpp`: PASS. + Commit: `fix(const-param): rewrite the parameter at the source index P0 gives`. +- [ ] **Step 3: Remove `debugArgNo`.** `ConstParamIssue::debugArgNo`, its padding and its assignment in `ConstParamAnalysis.cpp` go: nothing reads them. + Commit: `refactor(const-param): drop the debug number that the source index replaces`. +- [ ] **Step 4: Task tests.** As Task 1.1, Step 6. + +**Validation:** D1 passes; explicit-object unchanged; every T v1 test passes. + +### Task 1.3: planning and plan scope (§5.5) + +**Files:** `src/analysis/ConstSuggestionPlan.cpp` (new), `src/analysis/ConstSuggestionCheck.cpp`, `src/analysis/ConstSuggestionInternal.hpp`, `CMakeLists.txt`, `test/pointer_reference-const_correctness/const-suggestion-macro-shared-type.c` (new), `run_test.py`. + +**Interfaces.** Produces: +```cpp +// The macro definitions a compilation reads, by file: from each macro's name to its last token, +// as offsets (D11). A FileID, not a real path: the planning reads them in the same compilation. +using MacroDefinitions = std::map>>; +// Records them, for the compilation that plans (§5.5). +std::unique_ptr recordMacroDefinitions(const clang::SourceManager& sm, + MacroDefinitions& out); +// Plans the rewrite of the parameter at `sourceIndex` (P0's, none for an artificial binding) of +// `function`, located in the defining unit's initial compilation by the suggestion's symbol +// (T v1's GetDeclForMangledName). +SuggestionPlan plan(clang::ASTContext& context, const MacroDefinitions& macros, + const clang::FunctionDecl& function, std::optional sourceIndex); +``` +`Edit`, `realPath` and `writtenType` move to `ConstSuggestionInternal.hpp`. `SuggestionPlan` holds here `edits`, `expectedType`, `indeterminate`, `sourceIndex` and `declarableElsewhere`, which leave `FunctionFacts`; Task 1.4 adds `entity`, Task 1.8 `candidate`. + +**Tests.** D5, a new fixture (t3/m.c): + +`test/pointer_reference-const_correctness/const-suggestion-macro-shared-type.c`: + +```c +// SPDX-License-Identifier: Apache-2.0 +// #217: a macro argument can write the type of several parameters. Rewriting the type of `src` +// where it is written would rewrite `dst` too, which copy_one writes through: the rewrite would +// reach beyond the suggestion. The suggestion is kept, not checked. + +// strict-expectation-details: true + +#define DECLARE_COPY(name, T) int name(T src, T dst) + +// at line 15, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'src' in function 'copy_one' is never used to modify the pointed object +// ↳ current type: int *src +// ↳ suggested type: const int *src +// ↳ not checked with Clang: the parameter's type is written in a macro argument that writes other parameters too +DECLARE_COPY(copy_one, int*) +{ + *dst = *src; + return 0; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check +``` + +D11 (t13), a third case of `check_const_suggestion_line_endings`: spaces between the backslash and the line break, which Clang reads as a continuation, with a warning. Run as a check, not a fixture: a fixture's trailing spaces would not survive formatting and review. + +```diff +--- a/run_test.py ++++ b/run_test.py +@@ -2,8 +2,10 @@ + """ + #217: a type written on a continuation line of a #define is in the macro definition, whether +- the file ends its lines with LF or CRLF: rewriting it would change every expansion of the macro, +- here a function that writes through p. The suggestion is kept as not checked with both. ++ the file ends its lines with LF or CRLF, or puts spaces between the backslash and the line ++ break, which Clang accepts with a warning: rewriting it would change every expansion of the ++ macro, here a function that writes through p. The suggestion is kept as not checked in each ++ case. + """ +- print("=== Testing a macro definition's continuation lines, with LF and CRLF ===") ++ print("=== Testing a macro definition's continuation lines, with LF, CRLF and spaces ===") + source = ( + "#define PARAM(type) type p\n" +@@ -16,7 +18,11 @@ + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_line_endings_") as tmp: +- for label, newline in (("LF", "\n"), ("CRLF", "\r\n")): +- path = Path(tmp).resolve() / f"reader-{label.lower()}.c" +- path.write_bytes(source.replace("\n", newline).encode("utf-8")) ++ for label, contents in ( ++ ("LF", source), ++ ("CRLF", source.replace("\n", "\r\n")), ++ ("spaces after the backslash", source.replace("\\\n", "\\ \n")), ++ ): ++ path = Path(tmp).resolve() / f"reader-{label.lower().replace(' ', '-')}.c" ++ path.write_bytes(contents.encode("utf-8")) + # The file's bytes are an input the cache key does not cover. + result = run_analyzer_uncached( +``` + +Failure at `64ed37e`: both suggestions are removed. D5: the edit of the macro argument rewrites `dst` too, which copy_one writes. D11: the text scan does not see the continuation, so the macro body is rewritten, and so is write_second. + +```text +new/const-suggestion-macro-shared-type.c: FAIL (2/4) +=== Testing evidence/217/stage1/tests/fixtures/new/const-suggestion-macro-shared-type.c === + ❌ (default) expectation #1 MISSING + ❌ (smt-z3) expectation #1 MISSING +``` + +```text +check_const_suggestion_line_endings: FAIL +=== Testing a macro definition's continuation lines, with LF, CRLF and spaces === + ✅ LF + ✅ CRLF + ❌ spaces after the backslash: None +``` + +`const-suggestion-macros.c` and `const-suggestion-macro-params.c` stay unchanged and pass at `64ed37e`. + +- [ ] **Step 1: Add both tests and run them.** Run: the one-fixture command for `const-suggestion-macro-shared-type.c`, and the one-check command for `check_const_suggestion_line_endings`. Expected: FAIL, as above. + Commits: `test(const-param): keep a suggestion whose macro argument types two parameters`; `test(const-param): a backslash and spaces still continue a macro definition`. +- [ ] **Step 2: Move the planning.** `Planner`, `writtenType`, `expectedType` and the body of `Inspector::plan` move to `ConstSuggestionPlan.cpp`, behind `plan()`; `FunctionFacts` keeps what the other compilations fill. No behavior changes. + Run: the T v1 macro fixtures. Expected: PASS. Commit: `refactor(const-param): move the planning of a suggestion to its own file`. +- [ ] **Step 3: Macro definitions from the preprocessor (D11).** `recordMacroDefinitions` returns a `clang::PPCallbacks` whose `MacroDefined` records `[getDefinitionLoc(), getDefinitionEndLoc()]` of each macro written in a file (its FileID, offsets): no real path per macro. `InspectAction::CreateASTConsumer` adds it to the preprocessor of the defining unit's initial compilation. `Planner::replace` asks the recorded ranges whether the offset it rewrites lies in a macro definition; the text scan `inMacroDefinition` goes. + Run: `check_const_suggestion_line_endings`. Expected: PASS. Commit: `fix(const-param): find macro definitions with the preprocessor, not the text`. +- [ ] **Step 4: The plan's scope (D5).** An edit whose position comes from a macro argument (`isMacroArgExpansion`) serves only the parameter planned. Only then, a `RecursiveASTVisitor` over the translation unit checks every other `ParmVarDecl`: one with a part of its type (`TypeLoc::getNextTypeLoc` chain) written in a macro argument at an edit's spelling location gives `the parameter's type is written in a macro argument that writes other parameters too`; the plan is then indeterminate and nothing is rewritten. A comparison of spelled ranges does not do: a type whose first token a macro definition writes (`BYTE`) and whose last one the use site writes spans the file, and `argument_macro` of `const-suggestion-macros.c` then fails. Another declaration than a parameter (a variable the invocation declares) is left to the scope after the rewrite, Task 1.4's step 3, which its test pins (ruling 16). + Run: the one-fixture command for `const-suggestion-macro-shared-type.c`. Expected: PASS. Commit: `fix(const-param): leave a rewrite that reaches another parameter unchecked`. +- [ ] **Step 5: Task tests.** As Task 1.1, Step 6. + +**Validation:** D5 and D11 pass; T v1's macro tests unchanged. + +### Task 1.4: compilations, facts R, O and K, scope after the rewrite (§5.3, §5.4, §5.6), judgement steps 1 to 6 + +**Files:** `src/analysis/ConstSuggestionFacts.cpp` (new), `src/analysis/ConstSuggestionCheck.cpp`, `src/analysis/ConstSuggestionPlan.cpp`, `src/analysis/ConstSuggestionInternal.hpp`, `CMakeLists.txt`, `run_test.py`, `test/pointer_reference-const_correctness/const-suggestion-facts-without-code-change.cpp` (new). + +**Interfaces.** Adds `SourcePosition`, `DeclarationSite`, `SourceLinkage`, `EntityIdentity`, `FactKind`, `FactKey`, `Fact` (Shared types) and `SuggestionPlan::entity`. Produces: +```cpp +// What one compilation shows (§5.3, §5.6). R and K cover the whole unit; the rest is kept for +// each plan given, in their order. +struct UnitFacts +{ + std::string failure; // the compilation could not run (T v1's Compilation::failure) + bool complete = false; // the AST holds the whole unit: no fatal error + std::vector diagnostics; // errors and tracked warnings, in Clang's order + std::shared_ptr code; // prepared for the comparator + std::map defined; // symbols with a body + std::vector facts; // R and K + std::map writtenTypes; // declarations of the rewritten files, by both positions + struct Entity + { + bool found = false; + std::vector symbols; // every variant's, in this compilation + std::vector sites; // its declarations here + std::vector overrides; // O, for the methods named like it + std::vector casts; // explicit casts of its address (Task 1.7) + std::vector stale; // T v1's "declared in : with type " + std::string printedSignature; // T v1's, for __PRETTY_FUNCTION__ + }; + std::vector entities; +}; +// Compiles `unit` with `remapped` files, and inspects it for `plans`. +UnitFacts inspect(const UnitConfiguration& unit, const std::map& remapped, + const std::vector& plans, bool timing); +``` +The defining unit's initial compilation also plans: `inspect` locates each suggestion's function by its symbol there, calls `plan()`, then collects the facts with the identities planned. + +**Tests.** D2 (t1) and D3 (t2, ruling 5), two new checks, registered after the checks of Task 1.1: + +```python +def check_const_suggestion_operator_used_elsewhere() -> bool: + """ + #217: a function is found by its declaration in every file that sees the change, not by its + name: an operator has no identifier. Here a member operator and a method, declared in a header, + defined in one file and called in another: both suggestions are checked in both files. + """ + print("=== Testing const suggestions for a member operator used in another file ===") + sources = { + "vec.hpp": "struct Vec\n{\n int x;\n bool operator==(Vec& o) const;\n" + " int dot(Vec& o) const;\n};\n", + "vec.cpp": '#include "vec.hpp"\n' + "bool Vec::operator==(Vec& o) const\n{\n return x == o.x;\n}\n" + "int Vec::dot(Vec& o) const\n{\n return x * o.x;\n}\n", + "use.cpp": '#include "vec.hpp"\n' + "int use(Vec a, Vec b)\n{\n return (a == b) + a.dot(b);\n}\n", + } + checked = "checked in the analyzed files only" + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_operator_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + run = _const_suggestion_run([str(root / "vec.cpp"), str(root / "use.cpp")]) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + ok = True + for label, function in (("operator==", "_ZNK3VeceqERS_"), ("dot", "_ZNK3Vec3dotERS_")): + texts = messages.get(function, []) + good = ( + len(texts) == 1 + and checked in texts[0] + and f"{function} parameter o: checked in 2 file(s)" in trace + ) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok +``` + +```python +def check_const_suggestion_homonyms_across_files() -> bool: + """ + #217: in C, a function with internal linkage and a function of the same name with external + linkage in another file are two functions: neither file declares the other's. Whichever of the + two gets the suggestion, the other file does not refute it, and it is checked. + """ + print("=== Testing const suggestions for C homonyms with internal and external linkage ===") + sources = { + "static-reads.c": "static int helper(int* p)\n{\n return *p;\n}\n" + "int use_helper(void)\n{\n int v = 1;\n return helper(&v);\n}\n", + "external-writes.c": "int helper(int* p)\n{\n *p = 2;\n return 0;\n}\n", + "external-reads.c": "int reader(int* p)\n{\n return *p;\n}\n", + "static-writes.c": "static int reader(int* p)\n{\n *p = 3;\n return 0;\n}\n" + "int use_reader(void)\n{\n int v = 1;\n return reader(&v);\n}\n", + } + checked = "checked in the analyzed files only" + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_homonyms_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + for label, files, function in ( + ( + "a static reader, an external writer", + ["static-reads.c", "external-writes.c"], + "helper", + ), + ( + "an external reader, a static writer", + ["external-reads.c", "static-writes.c"], + "reader", + ), + ): + run = _const_suggestion_run([str(root / f) for f in files]) + texts = run[0].get(function, []) if run else [] + good = len(texts) == 1 and checked in texts[0] + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok +``` + +The scope after the rewrite (§5.6), a third check: a header whose text another file reads as other declarations (ruling 11): + +```python +def check_const_suggestion_header_read_differently() -> bool: + """ + #217: a header can mean other declarations in another file, through a macro that file defines + otherwise. Here pair.h declares read_value in def.c, and a global pointer in slot.c: rewriting + the type pair.h writes changes that global's type too, although the generated code does not + show it. The rewrite reaches beyond the suggestion: it is kept, not checked. + """ + print("=== Testing a const suggestion whose header means other declarations elsewhere ===") + sources = { + "pair.h": "PAIR(read_value, int*)\n", + "def.c": '#define PAIR(name, type) int name(type p);\n#include "pair.h"\n#undef PAIR\n' + "int read_value(int* p)\n{\n return *p;\n}\n", + "slot.c": '#define PAIR(name, type) type name##_slot;\n#include "pair.h"\n', + } + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_xmacro_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + run = _const_suggestion_run([str(root / "def.c"), str(root / "slot.c")]) + expected = ( + "not checked with Clang: the rewrite also changes the type of read_value_slot " + f"({root / 'pair.h'}:1)" + ) + texts = run[0].get("read_value", []) if run else [] + good = len(texts) == 1 and expected in texts[0] + detail = "" if good else f": {texts}" + print(f" {'✅' if good else '❌'} the global slot.c declares{detail}") + print() + return good +``` + +A new fixture: facts refute without a change of the generated code. T v1 never sees these, since it compares only code: + +`test/pointer_reference-const_correctness/const-suggestion-facts-without-code-change.cpp`: + +```cpp +// SPDX-License-Identifier: Apache-2.0 +// #217: a call or a constant condition that the suggested type changes refutes the suggestion, +// even when the generated code stays the same: +// - sized: sizeof names sized(bool) before the rewrite and sized(const int*) after it; both return +// int, so the size, and the code, stay the same; +// - typed: if constexpr tests the parameter's own type, and both branches are the same code. + +// not contains: parameter 'p' in function 'sized(int*)' +// not contains: parameter 'p' in function 'typed(int*)' +#include + +int sized(int* p) +{ + return *p; +} + +int sized(bool flag); + +int size_of_call(const int* q) +{ + return static_cast(sizeof(sized(q))); +} + +int typed(int* p) +{ + if constexpr (std::is_const_v>) + return *p; + else + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check +``` + +Step 2 of §5.8, a new fixture (ruling 12): the suggested type selects a template whose instantiation never ends, and the compilation stops at that fatal error. The expectation stops after `compilation:`; the file, the line and Clang's message follow: + +`test/pointer_reference-const_correctness/const-suggestion-incomplete-inspection.cpp`: + +```cpp +// SPDX-License-Identifier: Apache-2.0 +// #217: the suggested type can make a compilation stop at a fatal error, after which the code it +// would have parsed is not inspected. Here sizeof(sized(p)) selects sized(int*) before the rewrite, +// and the template once p points to const: deducing its return type instantiates Deep without end. +// What the rewrite changes is then unknown: the suggestion is kept, not checked. + +// strict-expectation-details: true + +template struct Deep +{ + static constexpr int value = Deep::value + 1; +}; + +int sized(int* p); + +template auto sized(const T* p) +{ + return Deep::value + *p; +} + +// at line 26, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_sized(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ not checked with Clang: the rewritten code cannot be inspected entirely, a fatal error stops its compilation: +int read_sized(int* p) +{ + return *p + static_cast(sizeof(sized(p))); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check +``` + +The identity by both positions, a new fixture (ruling 14): one macro invocation declares two functions; the one suggested comes first in one invocation, second in the other: + +`test/pointer_reference-const_correctness/const-suggestion-macro-two-functions.c`: + +```c +// SPDX-License-Identifier: Apache-2.0 +// #217: one macro invocation can declare several functions, whose names it expands at the same +// place. Each one is also known by where its name is written: the check rewrites the function the +// suggestion names and leaves the other one, which writes through its parameter, whichever the +// invocation declares first. + +// strict-expectation-details: true + +#define DECLARE_TWO(a, Ta, b, Tb) \ + int a(Ta p); \ + int b(Tb q); + +DECLARE_TWO(read_first, int*, write_first, int*) +DECLARE_TWO(write_second, int*, read_second, int*) + +// at line 21, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_first' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int read_first(int* p) +{ + return *p; +} + +int write_first(int* q) +{ + *q = 1; + return 0; +} + +int write_second(int* p) +{ + *p = 2; + return 0; +} + +// at line 43, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'q' in function 'read_second' is never used to modify the pointed object +// ↳ current type: int *q +// ↳ suggested type: const int *q +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int read_second(int* q) +{ + return *q; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check +``` + +Failure at `64ed37e`. D2: `operator==` is removed, "the methods it overrides change": T v1 recognizes a method by its identifier after the rewrite, and an operator has none. D3: both removed, "declared in b.c:1 / d.c:1 with type int *": T v1 takes a homonym for the function, whatever its linkage. The header: checked, since the global's new type leaves the IR as it was. The facts' fixture: both kept as checked. The incomplete inspection: removed, `does not compile: …: recursive template instantiation exceeded maximum depth of 1024`, which a fatal error does not establish (step 2 comes before step 4). The two functions of one invocation pass at `64ed37e`: T v1 finds a function by its symbol. Their failure is shown on a mutant, in Step 3. + +```text +check_const_suggestion_operator_used_elsewhere: FAIL +=== Testing const suggestions for a member operator used in another file === + ❌ operator==: [] + ✅ dot +``` + +```text +check_const_suggestion_homonyms_across_files: FAIL +=== Testing const suggestions for C homonyms with internal and external linkage === + ❌ a static reader, an external writer: [] + ❌ an external reader, a static writer: [] +``` + +```text +check_const_suggestion_header_read_differently: FAIL +=== Testing a const suggestion whose header means other declarations elsewhere === + ❌ the global slot.c declares: ["\t[ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_value' is never used to modify the pointed object\n\t\t ↳ current type: int *p\n\t\t ↳ suggested type: con … +``` + +```text +new/const-suggestion-facts-without-code-change.cpp: FAIL (2/6) +=== Testing evidence/217/stage1/tests/fixtures/new/const-suggestion-facts-without-code-change.cpp === + ❌ (default) negative expectation #1 FOUND (unexpected) + ❌ (default) negative expectation #2 FOUND (unexpected) + ❌ (smt-z3) negative expectation #1 FOUND (unexpected) + ❌ (smt-z3) negative expectation #2 FOUND (unexpected) +``` + +```text +new/const-suggestion-incomplete-inspection.cpp: FAIL (2/4) +=== Testing evidence/217/stage1/tests/fixtures/new/const-suggestion-incomplete-inspection.cpp === + ❌ (default) expectation #1 MISSING + ❌ (smt-z3) expectation #1 MISSING +``` + +```text +new/const-suggestion-macro-two-functions.c: PASS (6/6) +=== Testing evidence/217/stage1/tests/fixtures/new/const-suggestion-macro-two-functions.c === + ✅ (default) expectation #1 FOUND + ✅ (default) expectation #2 FOUND + ✅ (smt-z3) expectation #1 FOUND + ✅ (smt-z3) expectation #2 FOUND +``` + +Unchanged and passing at `64ed37e`: `const-suggestion-same-code.cpp`, `const-suggestion-override-elsewhere.cpp`, `pick` in `const-suggestion-deduction-refused.cpp`, `generic_selection` in `const-suggestion-must-compile.c`. + +- [ ] **Step 1: Add the six tests and run them.** Expected: FAIL, as above, but for the two functions of one invocation, which pass. + Commits: `test(const-param): check a member operator's suggestion in the file that calls it`; `test(const-param): a C homonym with another linkage is another function`; `test(const-param): a changed call or constant refutes even with the same code`; `test(const-param): keep a rewrite that changes another file's declaration`; `test(const-param): keep a suggestion whose rewrite stops at a fatal error, unchecked`; `test(const-param): tell apart two functions that one macro invocation declares`. +- [ ] **Step 2: Move the compilation.** `DiagnosticCollector`, `Inspector`, `InspectAction`, `compile` and `scan` move to `ConstSuggestionFacts.cpp`. No behavior changes. Run: the T v1 checks and fixtures. Expected: PASS. + Commit: `refactor(const-param): move the compilation and its inspection to their own file`. +- [ ] **Step 3: The entity's identity (§3, §5.5).** `plan()` describes the located function: its redeclarations' sites (ruling 14: the expansion and the spelling location of each name, each as a real path and an offset), its formal linkage (`External` for external or module linkage, `Internal` for internal or unique-external, `None` otherwise), its template pattern's first declaration, and its variants' symbols, from `CodeGenerator::GetMangledName` (a constructor's `Ctor_Complete` and `Ctor_Base`, a destructor's `Dtor_Deleting`, `Dtor_Complete` and `Dtor_Base`, otherwise the function's). + In every other compilation, the entity is the function that has one of those sites, once mapped back to the original text through the plan's edits: + ```cpp + // An offset of the rewritten text, in the original one. An offset inside inserted text maps + // to the edit's position. + std::size_t originalOffset(const std::vector& sortedEdits, std::size_t offset) + { + std::size_t inserted = 0; // text the edits before `offset` add + std::size_t removed = 0; // and remove + for (const Edit& edit : sortedEdits) + { + const std::size_t at = edit.offset + inserted - removed; + if (offset < at) + break; + if (offset < at + edit.text.size()) + return edit.offset; + inserted += edit.text.size(); + removed += edit.length; + } + return offset - inserted + removed; + } + ``` + Both positions of a site are mapped back; a site matches only when both do. A unit that shares none of those sites declares the entity only through the linker: a function with external linkage whose symbol is one of the entity's. T v1's name and symbol test (`isTarget`, `locate`'s `withoutConst` search) goes. A unit sees the change (§5.2) through a reference in its analyzed module only if the entity has external linkage. + Run: both checks, and `const-suggestion-macro-two-functions.c`. Expected: PASS. Mutant: compare only the expansion position of a site; run the fixture. Expected: FAIL, one of the two suggested functions confused with the function its invocation declares beside it. Restore; record both outputs in `evidence/217/stage1/`. + Commit: `fix(const-param): find the function by its declarations, not by its name`. +- [ ] **Step 4: Facts R, O and K (§5.4).** The `Inspector` visits the whole unit, template instantiations included: + - R: `DeclRefExpr`, `MemberExpr`, `CXXConstructExpr`, `CXXNewExpr` and `CXXDeleteExpr` that name a function. Key: the instantiations around the expression (each by its symbol, outermost first), and its site, both positions mapped back. Value (ruling 4): the target's declaration sites, mapped back; for a template specialization or an implicit function, its symbol; + - O: for each method with the entity's `DeclarationName`, the methods it overrides (`overridden_methods()`), each by the same value as R. Key: the method's site; + - K: each `IfStmt` with `isConstexpr()` and a condition that is not value-dependent: `true` or `false`, from `EvaluateKnownConstInt`; each `GenericSelectionExpr` that is not result-dependent: `getResultIndex()`. + A symbol comes from `CodeGenerator::GetMangledName`, cached by declaration. A fact is compared only when its key's instantiations all map to the other compilation: here, through the entity's own symbol pairs (Task 1.5 adds the others). An R fact is unchanged when its value after contains its value before (ruling 4); O and K must be equal. T v1's comparison of overrides by qualified name goes. + Judgement step 6, after T v1's diagnostics (steps 4 and 5): the first changed fact refutes, with its text (Texts). + Run: the new fixture, then `const-suggestion-same-code.cpp`, `const-suggestion-override-elsewhere.cpp`, `const-suggestion-deduction-refused.cpp` and `const-suggestion-must-compile.c`. Expected: PASS. + Commit: `feat(const-param): refute a suggestion that changes a call, override or constant`. +- [ ] **Step 5: Scope after the rewrite (§5.6), steps 2 and 3.** Each compilation records `writtenTypes`: the canonical type (`writtenType`) of every declaration whose name's expansion location lies in a file that some plan rewrites (`DeclaratorDecl`, `TypedefNameDecl`), neither implicit nor instantiated, by its site, both positions (ruling 14). After the rewrite, every site keeps its type, except the planned parameters and the entity's redeclarations; a planned parameter must have the candidate (T v1's `the rewritten declaration does not get the suggested type`). Otherwise step 3: `the rewrite also changes the type of (:)`. A compilation after the rewrite with a fatal error (`hasFatalErrorOccurred()`) has an incomplete inspection, step 2: `the rewritten code cannot be inspected entirely, a fatal error stops its compilation: :: `, the first fatal error. T's compilations set no error limit (`DiagnosticOptions::ErrorLimit = 0`): "too many errors" would otherwise make a rewrite that breaks many uses incomplete instead of refuted, where T v1 refutes it (ruling 12). The judgement now runs steps 1 to 6 in the order of §5.8; T v1's code comparison stays step 7 until Task 1.5. + Run: `check_const_suggestion_header_read_differently`, `const-suggestion-incomplete-inspection.cpp`. Expected: PASS. Commit: `feat(const-param): leave a rewrite that changes another declaration unchecked`. +- [ ] **Step 6: Task tests.** As Task 1.1, Step 6. + +**Validation:** D2, D3, the header check and the three new fixtures pass; the mutant of Step 3 failed; the refutations by R, O and K of T v1's tests unchanged. + +### Task 1.5: correspondences and the comparator's pairs (§5.7, §5.9), judgement step 7 + +**Files:** `src/analysis/ConstSuggestionMatch.cpp` (new), `src/analysis/ConstSuggestionFacts.cpp`, `src/analysis/ConstSuggestionCode.cpp`, `src/analysis/ConstSuggestionCheck.cpp`, `src/analysis/ConstSuggestionInternal.hpp`, `include/analysis/ConstSuggestionCheck.hpp`, `CMakeLists.txt`, `test/unit/analyzer_module_unit_tests.cpp` (adapted, ruling 15), `test/pointer_reference-const_correctness/const-suggestion-inheriting-constructor.cpp` (new), `const-suggestion-deduction-refused.cpp` and `const-suggestion-printed-signature.c` (expectations changed). + +**Interfaces.** Adds `SymbolPair` (Shared types). `UnitFacts::Entity` gains what the correspondences read: +```cpp +// A symbol that the rewrite may rename with the entity, and what it is in this compilation. +struct Named +{ + std::string symbol; + std::string key; // what pairs it: its kind, its site mapped back, and its enclosing symbol +}; +std::vector variants; // the entity's: "C1", "C2", "D0"... or "" for a function +std::vector inheriting; // the constructors inheriting it: derived class, inherited + // specialization and variant (ruling 7) +std::vector nested; // lambdas, local classes (methods, _ZTI, _ZTS, _ZTV), static + // locals and their guards, in the entity or a specialization +``` +An R fact's value also carries `template:` when its target is a template specialization. Produces: +```cpp +// The pairs established between a unit's initial and modified compilations for one entity +// (§5.7). `missing` receives the first reason a correspondence could not be established. +std::vector correspond(const UnitFacts& before, const UnitFacts& after, + std::size_t entity, std::string& missing); +// ConstSuggestionCode: the pairs replace `rewritten` and `renamedTo` (ruling 9). +std::optional codeDifference(const ModuleText& was, const ModuleText& now, + const std::vector& established, + const PrintedSignature* signature, + std::vector* accepted); +``` +`SymbolPair` is declared in `ConstSuggestionCheck.hpp`, whose `rewrittenCodeDifference` takes the pairs the same way: +```cpp +std::optional rewrittenCodeDifference(llvm::Module& before, llvm::Module& after, + const std::vector& established, + const PrintedSignature* signature = nullptr, + std::vector* accepted = nullptr); +``` + +**Tests.** D7, a new fixture (t8 and three template cases, the constructor template inherited for `int*` and for `double*`, ruling 7): + +`test/pointer_reference-const_correctness/const-suggestion-inheriting-constructor.cpp`: + +```cpp +// SPDX-License-Identifier: Apache-2.0 +// #217: a class that inherits a constructor (`using Base::Base`) gets constructors of its own, +// whose symbols name the inherited constructor's parameter types: the rewrite renames them too. +// The check pairs them before and after the rewrite through the constructor they inherit, so +// each suggestion is checked: a plain class, a class template as the base, a class template as +// the derived class, and a constructor template, inherited twice, for int* and for double*: the +// derived class inherits two constructors, one from each specialization. + +// strict-expectation-details: true + +// at line 19, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'Base::Base(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +struct Base +{ + int v; + Base(int* p) : v(*p) {} +}; + +struct Derived : Base +{ + using Base::Base; + int w = 0; +}; + +int make_derived(int x) +{ + Derived d(&x); + return d.v; +} + +// at line 42, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'TemplateBase::TemplateBase(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +template struct TemplateBase +{ + T v; + TemplateBase(T* p) : v(*p) {} +}; + +struct FromTemplate : TemplateBase +{ + using TemplateBase::TemplateBase; + int w = 0; +}; + +int make_from_template(int x) +{ + FromTemplate d(&x); + return d.v; +} + +// at line 65, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'PlainBase::PlainBase(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +struct PlainBase +{ + int v; + PlainBase(int* p) : v(*p) {} +}; + +template struct TemplateDerived : PlainBase +{ + using PlainBase::PlainBase; + T w = 0; +}; + +int make_template_derived(int x) +{ + TemplateDerived d(&x); + return d.v; +} + +// at line 94, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'GenericBase::GenericBase(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// at line 94, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'GenericBase::GenericBase(double*)' is never used to modify the pointed object +// ↳ current type: double *p +// ↳ suggested type: const double *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +struct GenericBase +{ + int v; + template GenericBase(T* p) : v(*p) {} +}; + +struct FromGeneric : GenericBase +{ + using GenericBase::GenericBase; + int w = 0; +}; + +int make_from_generic(int x) +{ + FromGeneric d(&x); + return d.v; +} + +int make_from_generic_double(double x) +{ + FromGeneric d(&x); + return d.v; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check +``` + +D14, `call` in `const-suggestion-deduction-refused.cpp`: kept, not checked, the reason naming the specializations; `pick` stays removed: + +```diff +--- a/test/pointer_reference-const_correctness/const-suggestion-deduction-refused.cpp ++++ b/test/pointer_reference-const_correctness/const-suggestion-deduction-refused.cpp +@@ -1,14 +1,29 @@ + // SPDX-License-Identifier: Apache-2.0 + // #217: specializations of the rewritten template whose deduced type changes are paired only when +-// the correspondence is unique, and their code is still compared. ++// the correspondence is one to one, and what they select is still compared. + // - call: g passes G& and h passes const G&; after the rewrite both deduce G, so two +-// specializations become one. No unique correspondence: both suggestions are removed. The +-// lambdas are noexcept, so that all three specializations get the same attributes: only the +-// correspondence tells them apart. +-// - pick: the lvalue's specialization is paired, but its code changes with what F deduces. ++// specializations become one. Without a one-to-one correspondence, nothing shows that the ++// rewrite keeps or changes what they do: both suggestions are kept, not checked, and the reason ++// names the specializations. The lambdas are noexcept, so that all three specializations get ++// the same attributes: only the correspondence tells them apart. ++// - pick: the lvalue's specialization is paired, and the condition of its `if constexpr` changes ++// with what F deduces: the suggestion is removed. + +-// not contains: in function 'int call< ++// strict-expectation-details: true + // not contains: in function 'int pick< + ++// at line 28, column 0 ++// [ !Info! ] ConstParameterNotModified.ReferenceRvaluePreferValue: parameter 'f' in function 'int call(from_temporary()::$_0&&)' is an rvalue reference and is only read, or passed where a reference to const is expected ++// ↳ consider passing by value ( f) or const reference (const &f) ++// ↳ current type: &&f ++// ↳ the callee's body is not checked: it may still modify the referred object through a cast ++// ↳ not checked with Clang: 'int call(from_lvalues()::$_0&)' and 'int call(from_lvalues()::$_0 const&)' both become 'int call(from_lvalues()::$_0 const&)': their correspondence is not one to one ++ ++// at line 28, column 0 ++// [ !Info! ] ConstParameterNotModified.Reference: parameter 'f' in function 'int call(from_lvalues()::$_0&)' is only read, or passed where a reference to const is expected ++// ↳ current type: &f ++// ↳ suggested type: const &f ++// ↳ the callee's body is not checked: it may still modify the referred object through a cast ++// ↳ not checked with Clang: 'int call(from_lvalues()::$_0&)' and 'int call(from_lvalues()::$_0 const&)' both become 'int call(from_lvalues()::$_0 const&)': their correspondence is not one to one + template int call(F&& f) + { +``` + +D16, `signature_size` in `const-suggestion-printed-signature.c`: kept, not checked. The expectation stops after `signature_size:`, the comparator's text that follows names IR values: + +```diff +--- a/test/pointer_reference-const_correctness/const-suggestion-printed-signature.c ++++ b/test/pointer_reference-const_correctness/const-suggestion-printed-signature.c +@@ -2,8 +2,8 @@ + // #217: __PRETTY_FUNCTION__ prints the function's signature, which the suggested type changes. The + // suggestion is kept, and the message says what the program prints differently. Any other use of +-// that text, such as its size, is a change of the generated code. ++// that text, such as its size, changes the generated code in a way that no reference, override or ++// constant condition explains: that suggestion is kept, not checked. + + // strict-expectation-details: true +-// not contains: parameter 'p' in function 'signature_size' + // not contains: ↳ ABI: + #include +@@ -21,4 +21,9 @@ + } + ++// at line 28, column 0 ++// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'signature_size' is never used to modify the pointed object ++// ↳ current type: int *p ++// ↳ suggested type: const int *p ++// ↳ not checked with Clang: the generated code changes, and no reference, override or constant condition explains it: code changed in signature_size: + int signature_size(int* p) + { +``` + +The comparator's unit tests, adapted to the established pairs (ruling 15): + +```diff +--- a/test/unit/analyzer_module_unit_tests.cpp ++++ b/test/unit/analyzer_module_unit_tests.cpp +@@ -751,24 +751,12 @@ + } + +- /// #217: a symbol's demangled name without const keeps the identifiers that contain "const": +- /// get_const(int*) and get_(int*) are two functions. +- bool testConstSuggestionWithoutConstKeepsIdentifiers(TestReport& report) +- { +- using ctrace::stack::analysis::const_suggestion::withoutConst; +- report.expect(withoutConst("_Z9get_constPi") != withoutConst("_Z4get_Pi"), +- "ConstSuggestionCode: get_const(int*) and get_(int*) stay distinct"); +- report.expect(withoutConst("_Z14const_iteratorPKi") == "const_iterator(int*)", +- "ConstSuggestionCode: const_iterator keeps its name, the qualifier goes"); +- report.expect(withoutConst("_Z1fPKi") == withoutConst("_Z1fPi"), +- "ConstSuggestionCode: f(const int*) and f(int*) have the same key"); +- return report.failures == 0; +- } +- + /// #217: the code generated before and after a const rewrite is compared once the symbols +- /// the rewrite renames are paired. A symbol the unit only declares has no code to compare: it +- /// is paired only if it is the rewritten function itself. ++ /// the rewrite renames are paired, by the pairs its correspondences established and by them ++ /// only: a symbol renamed without a pair, even one whose name differs only by const, is a ++ /// difference. + bool testConstSuggestionCodeComparison(TestReport& report) + { + using ctrace::stack::analysis::rewrittenCodeDifference; ++ using ctrace::stack::analysis::SymbolPair; + llvm::LLVMContext context; + llvm::SMDiagnostic error; +@@ -816,9 +804,13 @@ + return false; + } +- report.expect(!rewrittenCodeDifference(*calledBefore, *calledAfter, "_Z1fPi"), ++ const std::vector function = {{"_Z1fPi", "_Z1fPKi"}}; ++ report.expect(!rewrittenCodeDifference(*calledBefore, *calledAfter, function), + "ConstSuggestionCode: the rewritten function and its caller are renamed " + "together"); +- report.expect(rewrittenCodeDifference(*thrownBefore, *thrownAfter, "_Z1fPi").has_value(), +- "ConstSuggestionCode: typeinfo for int* and for int const*, only declared, " ++ report.expect(rewrittenCodeDifference(*calledBefore, *calledAfter, {}).has_value(), ++ "ConstSuggestionCode: a function renamed without an established pair is a " ++ "difference"); ++ report.expect(rewrittenCodeDifference(*thrownBefore, *thrownAfter, function).has_value(), ++ "ConstSuggestionCode: typeinfo for int* and for int const*, without a pair, " + "are not paired"); + return report.failures == 0; +@@ -841,5 +833,5 @@ + if (!was || !now) + return std::optional(); +- return std::optional(rewrittenCodeDifference(*was, *now, "f").has_value()); ++ return std::optional(rewrittenCodeDifference(*was, *now, {}).has_value()); + }; + const auto replaced = [](std::string text, const std::string& from, const std::string& to) +@@ -2894,5 +2886,4 @@ + (void)testAnalysisIncompleteNotesHaveTheirOwnRules(report); + (void)testConstSuggestionCodeComparison(report); +- (void)testConstSuggestionWithoutConstKeepsIdentifiers(report); + (void)testConstSuggestionNormalizations(report); + (void)testUnresolvedCallsMarkStackUnknown(repoRoot, report); +``` + +Failure at `64ed37e`: the five inherited constructors and both `call` suggestions are removed ("symbols no longer used"), `signature_size` too ("the generated code changes"). + +```text +new/const-suggestion-inheriting-constructor.cpp: FAIL (2/12) +=== Testing evidence/217/stage1/tests/fixtures/new/const-suggestion-inheriting-constructor.cpp === + ❌ (default) expectation #1 MISSING + ❌ (default) expectation #2 MISSING + ❌ (default) expectation #3 MISSING + ❌ (default) expectation #4 MISSING + ❌ (default) expectation #5 MISSING + ❌ (smt-z3) expectation #1 MISSING + ❌ (smt-z3) expectation #2 MISSING + ❌ (smt-z3) expectation #3 MISSING + ❌ (smt-z3) expectation #4 MISSING + ❌ (smt-z3) expectation #5 MISSING +``` + +```text +changed/const-suggestion-deduction-refused.cpp: FAIL (4/8) +=== Testing evidence/217/stage1/tests/fixtures/changed/const-suggestion-deduction-refused.cpp === + ❌ (default) expectation #1 MISSING + ❌ (default) expectation #2 MISSING + ✅ (default) negative expectation #1 NOT FOUND (as expected) + ❌ (smt-z3) expectation #1 MISSING + ❌ (smt-z3) expectation #2 MISSING + ✅ (smt-z3) negative expectation #1 NOT FOUND (as expected) +``` + +```text +changed/const-suggestion-printed-signature.c: FAIL (6/8) +=== Testing evidence/217/stage1/tests/fixtures/changed/const-suggestion-printed-signature.c === + ✅ (default) expectation #1 FOUND + ❌ (default) expectation #2 MISSING + ✅ (default) negative expectation #1 NOT FOUND (as expected) + ✅ (smt-z3) expectation #1 FOUND + ❌ (smt-z3) expectation #2 MISSING + ✅ (smt-z3) negative expectation #1 NOT FOUND (as expected) +``` + +Unchanged and passing at `64ed37e`: `const-suggestion-kept*.cpp`, `const-suggestion-kept.c`, `const-suggestion-abi*.cpp`, `const-suggestion-deduction-kept.cpp`, `print_and_read` in `const-suggestion-printed-signature.c`. Their renamed symbols, measured before and after the rewrite (`evidence/217/stage1/nested-probe/symbols.txt`): the function; `_ZTI` and `_ZTS` of a local class; a lambda's call operator; the specializations of the rewritten template and the lambda instantiated in each. + +- [ ] **Step 1: Add the fixture, change the two expectations, run the three.** Expected: FAIL, as above. + Commits: `test(const-param): check a suggestion through the constructors that inherit it`; `test(const-param): keep the suggestions whose specializations merge, unchecked`; `test(const-param): keep a suggestion whose code change nothing explains, unchecked`. +- [ ] **Step 2: Symbols from compilation facts.** The `Inspector` records, in every compilation: + - `variants`: the entity's variants, by `GlobalDecl` (Task 1.4, Step 3), keyed by variant; + - `inheriting`: each `CXXConstructorDecl` with `isInheritingConstructor()` whose `getInheritedConstructor().getConstructor()` is the entity, or a specialization of its pattern; its `Ctor_Complete` and `Ctor_Base` symbols, keyed by the derived class (its mangled type, `MangleContext::mangleCanonicalTypeName`), the specialization it inherits (the template arguments of `getInheritedConstructor().getConstructor()`, each written canonically as `writtenType` does, none for a constructor that is no template) and the variant. `evidence/217/stage1/d7-probe/` shows these symbols equal to the IR's, before and after, in the five cases of the fixture; + - `nested`: in the entity, and in each function that a specialization of its template defines, every lambda's call operator, every local class's methods (by variant) and its `_ZTI`, `_ZTS` and `_ZTV` (`MangleContext::mangleCXXRTTI`, `mangleCXXRTTIName`, `mangleCXXVTable`), every static local variable and its guard (`mangleStaticGuardVariable`); keyed by kind, site mapped back, and the enclosing function's symbol. `evidence/217/stage1/d7-probe/nested-ast.txt` shows these symbols equal to the IR's (`nested-output.txt`). + Commit: `feat(const-param): record the symbols a rewrite may rename, with their declarations`. +- [ ] **Step 3: `correspond` (§5.7)**, in `ConstSuggestionMatch.cpp`, in this order: + 1. the entity's variants, by variant; + 2. the inheriting constructors, by derived class, inherited specialization and variant; + 3. the instantiation contexts, from the R facts whose instantiations already map. For each such fact whose target changes, the pair (target before, target after) is a candidate if both are specializations of one template (`template:` equal). The candidates of every fact are then checked together: a target before with two targets after, or two targets before with one after, gives `missing` (Texts, §5.7), and no pair for them. Each new pair maps more instantiations, and the step repeats until no pair is added; + 4. the nested symbols, by key, once their enclosing symbols are paired. + A pair is established only from these facts; the names of the symbols play no part. + Commit: `feat(const-param): establish the correspondences of a rewrite from its facts`. +- [ ] **Step 4: The comparator takes the pairs, step 7.** First the unit tests: adapt `analyzer_module_unit_tests.cpp` as above; the unit-test target no longer compiles, `rewrittenCodeDifference` taking no pairs yet. Give `rewrittenCodeDifference` and `codeDifference` the pairs, still pairing by names as well (the mutant of this step): the unit tests build, and "a function renamed without an established pair is a difference" FAILS, the name heuristics pairing `_Z1fPi` and `_Z1fPKi`. Record that output in `evidence/217/stage1/`. Then `codeDifference` renames the symbols of `established` before comparing; every symbol that only one side has, and that no pair covers, is listed in its difference, as T v1 lists it (`symbols no longer used: …; symbols used only after the rewrite: …`). The masked encodings, `withoutConst`, `variantOf`, `templateOf` and `namesTemplate` go (ruling 9). The ABI line takes the symbol after the rewrite from the pair of the suggestion's own symbol. In the judgement, `correspond` runs before step 6: the facts are compared through its pairs, and those in an instantiation it could not pair are left out. After step 6, its `missing` makes the unit indeterminate; otherwise a difference of the comparator makes it indeterminate with `the generated code changes, and no reference, override or constant condition explains it: `. Step 7 never refutes. + Run: the unit tests, the three tests of Step 1, then every T v1 fixture with the option. Expected: PASS; each T v1 "checked" stays checked; `pick` is refuted by its K fact, the R and O refutations of `const-suggestion-same-code.cpp` and `const-suggestion-override-elsewhere.cpp` by their facts. + Commit: `fix(const-param): compare the code through established pairs, never refuting`. +- [ ] **Step 5: Task tests.** As Task 1.1, Step 6. + +**Validation:** D7, D14, D16 and the adapted unit tests pass, the latter after failing on the mutant; every T v1 "checked" stays verified. + +### Task 1.6: collisions, stale declarations, link-edition assumption (§5.10) + +**Files:** `src/analysis/ConstSuggestionMatch.cpp`, `src/analysis/ConstSuggestionFacts.cpp`, `src/analysis/ConstSuggestionCheck.cpp`, `README.md`, `run_test.py`. + +**Interfaces.** `UnitFacts::Entity` gains: +```cpp +// After the rewrite: a symbol the entity takes that another entity defines here, or another +// entity's declaration merged into it (ruling 3). +struct Collision +{ + std::string symbol; + DeclarationSite site; // the other entity's definition, or declaration + std::optional linkage; // none for a declaration +}; +std::vector collisions; +``` +and produces, in `ConstSuggestionMatch.cpp`: +```cpp +// §5.10: the verdict of the collisions that every unit found, after the units' judgements. +Judgement judgeCollisions(const std::vector& units, std::size_t entity, + const std::string& definingFile); +``` + +**Tests.** D4 (t9), the other file analyzed or only in the compilation database: + +```python +def check_const_suggestion_overload_defined_elsewhere() -> bool: + """ + #217: rewritten, f(int*) takes the symbol of the overload f(const int*), which another file + defines. The two definitions collide only if the files are linked together, which the check + does not know: the suggestion is kept, not checked, and the reason states that assumption, + whether the other file is analyzed or only listed in the compilation database. + """ + print("=== Testing a const suggestion that takes the symbol of another file's overload ===") + sources = { + "api.hpp": "int f(int* p);\nint f(const int* p);\n", + "a.cpp": '#include "api.hpp"\nint f(int* p)\n{\n return *p;\n}\n', + "b.cpp": '#include "api.hpp"\nint f(const int* p)\n{\n return *p + 1;\n}\n', + } + claims = ("checked in the analyzed files only", "checked in every file of the compilation") + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_overload_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + database = root / "compile_commands.json" + database.write_text( + json.dumps( + [ + { + "directory": str(root), + "file": str(root / f), + "arguments": ["clang++", "-std=c++17", "-c", str(root / f)], + } + for f in ("a.cpp", "b.cpp") + ] + ) + ) + expected = ( + f"not checked with Clang: _Z1fPKi is also defined in {root / 'b.cpp'}; if " + f"{root / 'a.cpp'} and {root / 'b.cpp'} are linked together, the two definitions " + "collide" + ) + for label, args in ( + ("both files analyzed", [str(root / "a.cpp"), str(root / "b.cpp")]), + ( + "the other file only in the compilation database", + [str(root / "a.cpp"), f"--compile-commands={database}"], + ), + ): + run = _const_suggestion_run(args) + texts = run[0].get("_Z1fPi", []) if run else [] + good = ( + len(texts) == 1 + and expected in texts[0] + and not any(claim in texts[0] for claim in claims) + ) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok +``` + +Review Focus 3, a function defined in a header that two analyzed files include: + +```python +def check_const_suggestion_inline_in_a_header() -> bool: + """ + #217: a function defined in a header that two analyzed files include is defined in both: a C + `static inline` function, one function in each file, and a C++ `inline` function, one function + that both files define. Neither definition collides with the other: each suggestion is checked. + """ + print("=== Testing const suggestions for functions defined in a shared header ===") + sources = { + "peek.h": "static inline int peek_c(int* p)\n{\n return *p;\n}\n", + "one.c": '#include "peek.h"\n' + "int use_one(void)\n{\n int v = 1;\n return peek_c(&v);\n}\n", + "two.c": '#include "peek.h"\n' + "int use_two(void)\n{\n int v = 2;\n return peek_c(&v);\n}\n", + "peek.hpp": "inline int peek_cpp(int* p)\n{\n return *p;\n}\n", + "three.cpp": '#include "peek.hpp"\n' + "int use_three()\n{\n int v = 3;\n return peek_cpp(&v);\n}\n", + "four.cpp": '#include "peek.hpp"\n' + "int use_four()\n{\n int v = 4;\n return peek_cpp(&v);\n}\n", + } + checked = "checked in the analyzed files only" + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_inline_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + for label, files, function in ( + ("C, static inline", ["one.c", "two.c"], "peek_c"), + ("C++, inline", ["three.cpp", "four.cpp"], "_Z8peek_cppPi"), + ): + run = _const_suggestion_run([str(root / f) for f in files]) + texts = run[0].get(function, []) if run else [] + # One suggestion for the definition in each file. + good = len(texts) == 2 and all(checked in text for text in texts) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok +``` + +D17, the two changed expectations: + +```diff +--- a/run_test.py ++++ b/run_test.py +@@ -3,8 +3,9 @@ + #217: a suggested const type is reported only if every analyzed file still compiles with it, + once every declaration the defining file sees is rewritten. Another analyzed file that stores +- the function where a non-const parameter is expected, declares it itself, or converts its +- address with an explicit cast, removes the suggestion; a homonym with internal linkage in +- another file does not. A file that is not analyzed is not checked. The callback and cast files +- sort before the definition, the redeclaration after it. ++ the function where a non-const parameter is expected, or converts its address with an explicit ++ cast, removes the suggestion. One that declares it itself, without the header, keeps it, not ++ checked: whether that matters depends on how the files are linked. A homonym with internal ++ linkage in another file does not change it. A file that is not analyzed is not checked. The ++ callback and cast files sort before the definition, the redeclaration after it. + """ + return _check_cross_tu_rule( +@@ -17,5 +18,5 @@ + ("def and a caller", ["def.c", "caller.c"], ["read_value"]), + ("def and a callback of the old type", ["def.c", "callback.c"], []), +- ("def and a redeclaration", ["def.c", "redecl.c"], []), ++ ("def and a redeclaration", ["def.c", "redecl.c"], ["read_value"]), + ("def and an explicit cast", ["def.c", "cast.c"], []), + # Another file's homonym with internal linkage is another function. +``` + +```diff +--- a/run_test.py ++++ b/run_test.py +@@ -3,11 +3,12 @@ + #217: with a compilation database, a suggested const type is checked in every file the + database lists that sees the change, analyzed or not, with its own compile command. A file +- that is not analyzed and stores the function where a non-const parameter is expected, declares +- it itself without the header, or converts its address with an explicit cast, removes the +- suggestion, even listed after a file that cannot be checked. So does a file that takes the +- address of a method with the old type: it sees the change through the header alone. A file +- that sees the change but does not compile as it is, or cannot be preprocessed, keeps the +- suggestion as not checked, without the claim of the check. A file that does not see the change +- is not compiled. ++ that is not analyzed and stores the function where a non-const parameter is expected, or ++ converts its address with an explicit cast, removes the suggestion, even listed after a file ++ that cannot be checked. So does a file that takes the address of a method with the old type: it ++ sees the change through the header alone. A file that declares the function itself, without ++ the header, keeps the suggestion as not checked, and the reason names it and the link it ++ assumes. A file that sees the change but does not compile as it is, or cannot be preprocessed, ++ keeps the suggestion as not checked, without the claim of the check. A file that does not see ++ the change is not compiled. + """ + print("=== Testing const suggestions checked in the compilation database ===") +@@ -87,5 +88,14 @@ + ), + # Without the header: only the name of the function shows that it sees the change. +- ("a redeclaration, not analyzed", ["def.c", "redecl.c"], "read_value", None), ++ ( ++ "a redeclaration, not analyzed", ++ ["def.c", "redecl.c"], ++ "read_value", ++ "not checked with Clang: " + str(fixtures / f"{prefix}redecl.c") ++ + " declares read_value with its current type and does not see the rewrite: if " ++ + str(fixtures / f"{prefix}def.c") + " and " + str(fixtures / f"{prefix}redecl.c") ++ + " are linked together, that declaration no longer matches the rewritten " ++ "definition", ++ ), + ("an explicit cast, not analyzed", ["def.c", "cast.c"], "read_value", None), + ("a method alone", ["method.cpp"], method, checked), +``` + +Failure at `64ed37e`: t9 is "checked", in both runs; the redeclaration removes the suggestion, in both checks. Review Focus 3 passes at `64ed37e`: T v1 never sees a collision, so the test guards the exception of §5.10 ("a same entity defined in several files is no collision") against Task 1.6 itself. Its failure is shown on a mutant of this task's code (Step 4). + +```text +check_const_suggestion_overload_defined_elsewhere: FAIL +=== Testing a const suggestion that takes the symbol of another file's overload === + ❌ both files analyzed: ["\t[ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'f(int*)' is never used to modify the pointed object\n\t\t ↳ current type: int *p\n\t\t ↳ suggested type: const int *p\ … + ❌ the other file only in the compilation database: ["\t[ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'f(int*)' is never used to modify the pointed object\n\t\t ↳ current type: int *p\n\t\t ↳ s … +``` + +```text +check_const_suggestion_inline_in_a_header: PASS +=== Testing const suggestions for functions defined in a shared header === + ✅ C, static inline + ✅ C++, inline +``` + +```text +check_const_suggestion_cross_tu (Task 1.6 version): FAIL +=== Testing suggested const types checked in every analyzed file === + ✅ default, def alone: ['read_value'] + ✅ default, def and a caller: ['read_value'] + ✅ default, def and a callback of the old type: [] + ❌ default, def and a redeclaration: [], expected ['read_value'] + ✅ default, def and an explicit cast: [] + ✅ default, a static function and a static homonym: ['helper'] + ✅ default, an anonymous namespace's function and its homonym: ['_ZN12_GLOBAL__N_16helperEPi'] + ✅ smt-z3, def alone: ['read_value'] + ✅ smt-z3, def and a caller: ['read_value'] + ✅ smt-z3, def and a callback of the old type: [] + ❌ smt-z3, def and a redeclaration: [], expected ['read_value'] + ✅ smt-z3, def and an explicit cast: [] + ✅ smt-z3, a static function and a static homonym: ['helper'] + ✅ smt-z3, an anonymous namespace's function and its homonym: ['_ZN12_GLOBAL__N_16helperEPi'] +``` + +```text +check_const_suggestion_compilation_database (Task 1.6 version): FAIL +=== Testing const suggestions checked in the compilation database === + ✅ a caller + ✅ a callback of the old type, not analyzed + ❌ a redeclaration, not analyzed: None + ✅ an explicit cast, not analyzed + ✅ a method alone + ✅ a method's address of the old type, not analyzed + ✅ a file that does not compile as it is + ✅ a file that cannot be preprocessed + ✅ a file that cannot be checked, then a callback + ✅ a file that does not see the change +``` + +The static and anonymous-namespace homonyms of `check_const_suggestion_cross_tu` stay unchanged. + +- [ ] **Step 1: Add the two checks and the D17 expectations; run them.** Expected: D4 and the redeclaration cases FAIL, as above; Review Focus 3 passes. + Commits: `test(const-param): keep a rewrite that takes another file's symbol, unchecked`; `test(const-param): a function a shared header defines does not collide with itself`; `test(const-param): keep a suggestion that another file redeclares, unchecked`. +- [ ] **Step 2: Collisions (§5.10).** After the rewrite, each compilation looks up every symbol the entity takes (its variants after the rewrite) in its module: a definition whose declaration is not the entity's (Task 1.4's identity) is a collision, with the IR linkage of that definition; the defining unit gives the linkage of the entity's own. A declaration the entity's redeclarations gain, compared with the unit's initial compilation, is a merge (ruling 3). Once every unit is judged, and none refutes, `judgeCollisions` applies the table of §5.10: + - local linkage (`internal`, `private`): no collision; + - both strong (`external`): in the defining file, Clang reports a redefinition, refuted at step 4; in another file, indeterminate, `… the two definitions collide`; + - one definition strong (`external`), the other weak (`weak`, `weak_odr`, `linkonce`, `linkonce_odr`), whichever of the two is the entity's: indeterminate, `… the strong definition, in , is used and the weak one, in , is ignored`; + - both weak: indeterminate, `… the linker keeps one of the two weak definitions, and the check establishes neither which one nor that they are equivalent`; + - a merge with a declaration: indeterminate, after any definition (ruling 1). + A definition that is the entity's, such as an `inline` function of a rewritten header, is no collision. + Run: `check_const_suggestion_overload_defined_elsewhere`, `check_const_suggestion_inline_in_a_header`. Expected: PASS. + Commit: `feat(const-param): keep a rewrite that may collide with another file, unchecked`. +- [ ] **Step 3: Stale declarations (D17).** A unit that reads no rewritten file and declares the entity through the linker (Task 1.4, Step 3) makes the unit indeterminate, after steps 4 to 6: ` declares with its current type and does not see the rewrite: …`. T v1's removal `declared in : with type ` goes. + Run: `check_const_suggestion_cross_tu`, `check_const_suggestion_compilation_database`. Expected: PASS, but for the `cast.c` cases (Task 1.7). + Commit: `fix(const-param): keep a suggestion that another file redeclares, unchecked`. +- [ ] **Step 4: Failure of Review Focus 3 on a mutant.** Count the entity's own definition as another entity's in Step 2's lookup; run `check_const_suggestion_inline_in_a_header`. Expected: FAIL (`C++, inline`: indeterminate, `… the linker keeps one of the two weak definitions …`). Restore the code; expected: PASS. Record both outputs in `evidence/217/stage1/`. +- [ ] **Step 5: README (D3).** In "Const parameter suggestions checked with Clang", step 2: "A homonym with internal linkage (`static`, anonymous namespace) in another file is another function." becomes "A function with internal linkage (`static`, anonymous namespace) and a function of the same name in another file are two functions, whichever of them has internal linkage." The rest of the section is rewritten with the verdicts in Task 1.8. + Commit: `docs(readme): a static function and its homonym elsewhere are two functions`. +- [ ] **Step 6: Task tests.** As Task 1.1, Step 6. + +**Validation:** D4, Review Focus 3 and the D17 expectations pass; Review Focus 3 failed on the mutant. + +### Task 1.7: explicit address casts (D15) + +**Files:** `src/analysis/ConstSuggestionFacts.cpp`, `src/analysis/ConstSuggestionCheck.cpp`, `run_test.py`. + +**Tests.** The two changed expectations, on top of Task 1.6's: + +```diff +--- a/run_test.py ++++ b/run_test.py +@@ -3,9 +3,10 @@ + #217: a suggested const type is reported only if every analyzed file still compiles with it, + once every declaration the defining file sees is rewritten. Another analyzed file that stores +- the function where a non-const parameter is expected, or converts its address with an explicit +- cast, removes the suggestion. One that declares it itself, without the header, keeps it, not +- checked: whether that matters depends on how the files are linked. A homonym with internal +- linkage in another file does not change it. A file that is not analyzed is not checked. The +- callback and cast files sort before the definition, the redeclaration after it. ++ the function where a non-const parameter is expected removes the suggestion. One that declares ++ it itself, without the header, or converts its address with an explicit cast, keeps it, not ++ checked: whether that matters depends on how the files are linked, or on what the cast hides. ++ A homonym with internal linkage in another file does not change it. A file that is not ++ analyzed is not checked. The callback and cast files sort before the definition, the ++ redeclaration after it. + """ + return _check_cross_tu_rule( +@@ -19,5 +20,5 @@ + ("def and a callback of the old type", ["def.c", "callback.c"], []), + ("def and a redeclaration", ["def.c", "redecl.c"], ["read_value"]), +- ("def and an explicit cast", ["def.c", "cast.c"], []), ++ ("def and an explicit cast", ["def.c", "cast.c"], ["read_value"]), + # Another file's homonym with internal linkage is another function. + ( +``` + +```diff +--- a/run_test.py ++++ b/run_test.py +@@ -3,12 +3,12 @@ + #217: with a compilation database, a suggested const type is checked in every file the + database lists that sees the change, analyzed or not, with its own compile command. A file +- that is not analyzed and stores the function where a non-const parameter is expected, or +- converts its address with an explicit cast, removes the suggestion, even listed after a file +- that cannot be checked. So does a file that takes the address of a method with the old type: it +- sees the change through the header alone. A file that declares the function itself, without +- the header, keeps the suggestion as not checked, and the reason names it and the link it +- assumes. A file that sees the change but does not compile as it is, or cannot be preprocessed, +- keeps the suggestion as not checked, without the claim of the check. A file that does not see +- the change is not compiled. ++ that is not analyzed and stores the function where a non-const parameter is expected removes ++ the suggestion, even listed after a file that cannot be checked. So does a file that takes the ++ address of a method with the old type: it sees the change through the header alone. A file ++ that declares the function itself, without the header, keeps the suggestion as not checked, ++ and the reason names it and the link it assumes; so does a file that converts the function's ++ address with an explicit cast, and the reason names the cast. A file that sees the change but ++ does not compile as it is, or cannot be preprocessed, keeps the suggestion as not checked, ++ without the claim of the check. A file that does not see the change is not compiled. + """ + print("=== Testing const suggestions checked in the compilation database ===") +@@ -98,5 +98,11 @@ + "definition", + ), +- ("an explicit cast, not analyzed", ["def.c", "cast.c"], "read_value", None), ++ ( ++ "an explicit cast, not analyzed", ++ ["def.c", "cast.c"], ++ "read_value", ++ "not checked with Clang: " + str(fixtures / f"{prefix}cast.c") ++ + ":10 converts the address of read_value with an explicit cast", ++ ), + ("a method alone", ["method.cpp"], method, checked), + ( +``` + +Failure at `64ed37e`: both remove the suggestion (`its address is converted by an explicit cast`). The cast cases of the two checks: + +```text +check_const_suggestion_cross_tu: FAIL +=== Testing suggested const types checked in every analyzed file === + ❌ default, def and an explicit cast: [], expected ['read_value'] + ❌ smt-z3, def and an explicit cast: [], expected ['read_value'] +``` + +```text +check_const_suggestion_compilation_database: FAIL +=== Testing const suggestions checked in the compilation database === + ❌ an explicit cast, not analyzed: None +``` + +- [ ] **Step 1: Change the two expectations and run both checks.** Expected: FAIL on the `cast.c` cases. + Commit: `test(const-param): keep a suggestion whose address another file casts, unchecked`. +- [ ] **Step 2: The cast, named.** `VisitExplicitCastExpr` (T v1's, cast to `void` still excluded) records the cast's site in `UnitFacts::Entity::casts` instead of a problem that refutes. After steps 4 to 6, a recorded cast makes the unit indeterminate: `: converts the address of with an explicit cast`, the first cast in source order. + Run: both checks. Expected: PASS. `const-suggestion-void-cast.c`: unchanged, PASS. + Commit: `fix(const-param): keep a suggestion whose address is cast, unchecked`. +- [ ] **Step 3: Task tests.** As Task 1.1, Step 6. + +**Validation:** both checks pass with their new expectations. + +**Follow-up (ruling 17, confirmed by the user on 2026-10-21).** A cast in the defining file left that unit indeterminate, which stopped the judgement of the other units: the error of a file that the rewrite breaks went unseen. `check_const_suggestion_cast_in_the_defining_file` (`7534472`) analyzes a defining file that casts the function's address with the callback of the old type (`cross-tu-const-suggestion-callback.c`), and expects the suggestion removed, `removed, does not compile: :`. Failure at `af99724`: `not checked, :12 converts the address of read_value with an explicit cast`. The loop then stops at a refutation only (`a60a1b4`). The comment of `const-suggestion-void-cast.c` now says that a cast to another function type keeps the suggestion, not checked (`1f81438`); its expectations are unchanged. + +### Task 1.8: trace and README + +**Files:** `src/analysis/ConstSuggestionCheck.cpp`, `src/analysis/ConstSuggestionPlan.cpp`, `src/analysis/ConstSuggestionInternal.hpp`, `README.md`, `run_test.py`. + +**Interfaces.** Adds `Candidate` and `SuggestionPlan::candidate` (Shared types): `current` and `type` printed with the parameter's name (`QualType::print` with the name as placeholder, `const int *const p` for `PointerConstOnly`), `alternative` from the rule (Texts). + +**Tests.** A new check of the trace's fields, on one verified, one refuted and one indeterminate suggestion; `check_const_suggestion_removal_reason` unchanged: + +```python +def check_const_suggestion_trace() -> bool: + """ + #217: with --verbose, the trace gives each verdict's grounds after the line of its outcome: the + suggestion (rule, function, the parameter's index among the source's parameters), the + transformation (current type, candidate, alternative) and where it is written, each unit + checked with its configuration and its verdict, then the verdict and its reason. Here one + suggestion verified, one refuted and one not checked. + """ + print("=== Testing the trace of const suggestion verdicts ===") + source = ( + "int read_value(int* p)\n{\n return *p;\n}\n" + "static int read_slot(int* slot)\n{\n return *slot;\n}\n" + "int (*const stored)(int*) = read_slot;\n" + "#define DECLARE(name) int name(int* q)\n" + "DECLARE(read_macro)\n{\n return *q;\n}\n" + ) + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_trace_") as tmp: + path = Path(tmp).resolve() / "verdicts.c" + path.write_text(source) + run = _const_suggestion_run([str(path)]) + if run is None: + return fail_check("the analysis did not run") + _, trace, _ = run + file = re.escape(str(path)) + unit = rf"unit {file} \[[0-9a-f]+\]" + rule = r"ConstParameterNotModified\.Pointer" + expected = { + "verified": [ + r"read_value parameter p: checked in 1 file\(s\)", + rf"read_value parameter p, suggestion: {rule} on read_value, source index 0", + r"read_value parameter p, transformation: int \*p becomes const int \*p " + r"\(pointer to const\)", + rf"read_value parameter p, rewritten at: {file}:1", + rf"read_value parameter p, {unit}: verified", + r"read_value parameter p, verdict: verified", + ], + "refuted": [ + rf"read_slot parameter slot: removed, does not compile: {file}:9:", + rf"read_slot parameter slot, suggestion: {rule} on read_slot, source index 0", + r"read_slot parameter slot, transformation: int \*slot becomes const int \*slot " + r"\(pointer to const\)", + rf"read_slot parameter slot, rewritten at: {file}:5", + rf"read_slot parameter slot, {unit}: refuted: does not compile: {file}:9:", + rf"read_slot parameter slot, verdict: refuted: does not compile: {file}:9:", + ], + "not checked": [ + r"read_macro parameter q: not checked, the declaration is written by a macro " + r"definition", + rf"read_macro parameter q, suggestion: {rule} on read_macro, source index 0", + r"read_macro parameter q, transformation: int \*q becomes const int \*q " + r"\(pointer to const\)", + r"read_macro parameter q, verdict: indeterminate: the declaration is written by a " + r"macro definition", + ], + } + ok = True + for label, patterns in expected.items(): + missing = [p for p in patterns if not any(re.match(p, line) for line in trace)] + good = not missing + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": missing {missing}")) + ok = ok and good + if not ok: + print(" trace:\n" + "\n".join(f" {line}" for line in trace)) + print() + return ok +``` + +Failure at `64ed37e`: only T v1's line per suggestion. + +```text +check_const_suggestion_trace: FAIL +=== Testing the trace of const suggestion verdicts === + ❌ verified: missing ['read_value parameter p, suggestion: ConstParameterNotModified\\.Pointer on read_value, source index 0', 'read_value parameter p, transformation: int \\*p becomes const int \\*p \\(pointer to const … + ❌ refuted: missing ['read_slot parameter slot, suggestion: ConstParameterNotModified\\.Pointer on read_slot, source index 0', 'read_slot parameter slot, transformation: int \\*slot becomes const int \\*slot \\(pointer … + ❌ not checked: missing ['read_macro parameter q, suggestion: ConstParameterNotModified\\.Pointer on read_macro, source index 0', 'read_macro parameter q, transformation: int \\*q becomes const int \\*q \\(pointer to co … +``` + +- [ ] **Step 1: Add the check and run it.** Expected: FAIL, as above. + Commit: `test(const-param): give each verdict's grounds in the trace`. +- [ ] **Step 2: The trace (§4).** After T v1's line, `checkConstSuggestions` logs the `suggestion`, `transformation`, `rewritten at` and `verdict` lines (Texts), around Task 1.1's `unit` lines. A suggestion not planned has no `rewritten at` and no `unit` line. + Run: `check_const_suggestion_trace`, `check_const_suggestion_removal_reason`. Expected: PASS. + Commit: `feat(const-param): trace the suggestion, its rewrite, its units and its verdict`. +- [ ] **Step 3: README.** "Const parameter suggestions checked with Clang" describes: + - the three verdicts (§3): verified, with the "checked" line; refuted, removed, on an error or a tracked warning introduced, or a changed call, override or constant condition; indeterminate, kept with `not checked with Clang: `; + - steps 1 to 3 as T v2 runs them: the plan and its scope, the units that see the change, the judgement of §5.8; + - the limits of §10: collisions between files and the ODR not decided, one configuration per file, a change of behavior that R, O and K do not cover kept as not checked (`signature_size`), a cost to measure (stage 3). + The experimental notice keeps its sense; the sentence "A difference its normalizations do not cover removes a suggestion that is valid" becomes "A difference that the facts do not explain keeps the suggestion as not checked". + Commit: `docs(readme): describe the three verdicts of the const suggestion check`. +- [ ] **Step 4: Task tests.** As Task 1.1, Step 6. + +**Validation:** both checks pass. + +**Validation of stage 1:** the catalogue tests D1 (T), D2, D3, D4, D5, D7, D11, D14, D15, D16, D17, Review Focus 2 to 4, the tests that rulings 6, 11, 12, 13 and 14 add, and the unit tests that ruling 15 adapts, pass; every T v1 test passes, except the changed expectations of D14, D15, D16 and D17; the suite without and with the option, the unit tests, on macOS and Linux; the review cases t1 to t15 give the verdicts of §6 for the stage-1 IDs; CI green and code scanning triaged. + +**Pull requests opened after stage 1 (the user, 2026-10-21).** For CI and code scanning, the user had both branches published once stage 1 passed locally, ahead of the spec's calendar (§9, step 6, which opens the draft at the end): +- #238, draft, `237-parameter-binding-abi` at `d711321`, `Fixes #237`; +- #239, draft, `217-t-v2` at `00a21b7`, part of #217; it carries `d711321`. + +Both target `main`, and neither is merged; #221 stays the draft of T v1. From then on, each stage's validation also runs on #239: CI green, code scanning compared with `main`, the triage summarized in the description, which states what remains. Task F.3 keeps the description up to date instead of opening the pull request. + +## Stage 2 — D6, D8, D10, D12, D13, D18 + +Detailed at the stage's start, on `4fd0dae` (2026-10-22). Stage 1 is validated at that head on macOS, Linux arm64 and Linux x86_64: built without warning, unit and ownership tests passed, suite 2645/2645 on each. The CI of #239 is green there, macOS 14 included. Catalogue: D6, D8, D10, D12, D13, and D18, found while detailing the stage and added to Task 2.2 by the user (spec §6); Review Focus 5. Evidence: `coretrace-stack-analyzer-evidence/217/stage2/`. + +The stage runs in four batches, one per task, in this order: 2.1, 2.2, 2.4, 2.3 (ruling 4). Each batch's tests are written out with their failure at the batch's base, and are authorized before they are added. Batch 2.1's are written out below; the others' are written out when their batch starts. + +### Texts + +A new reason of an indeterminate verdict, after `not checked with Clang: `: + +| Reason | Task | When | +|---|---|---| +| `the qualifier of the parameter's typedef is written by a macro` | 2.3 | D10: a qualifier written apart from a pointer typedef, by a macro | + +The type displayed (Task 2.4, ruling 3). This applies to a `ReferenceRvaluePreferValue` or `PointerConstOnly` suggestion whose plan has a candidate, whatever its verdict. `` is the plan's candidate, written as ruling 3 says: +- **`ReferenceRvaluePreferValue`, the advice.** `consider passing by value () or const reference ()` becomes `consider passing by const reference ()`. +- **`ReferenceRvaluePreferValue`, the "checked" line of a verified suggestion:** + ```text + checked in the analyzed files only, with their compile commands: they compile with the suggested const reference, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside; passing by value is not checked + ``` + The database variant keeps its own opening, `checked in every file of the compilation database that sees the change, with its compile command: `. +- **`PointerConstOnly`.** `consider '' for API const-correctness` and `suggested type: `, for example `const int *const p` where the rule writes `const int *p`. + +The other rules keep their lines. Without the option, nothing changes. + +### Rulings + +1. **The tracked groups as Clang reports them by default** (Task 2.1). Validated by the user on 2026-10-22; spec §5.3 updated. + - The plan and §5.3 wrote `-W` then `-Wno-error=`. But `-Wno-error=` also turns into warnings the diagnostics that Clang reports as errors by default. With Clang 20, in C, these are the diagnostics of `incompatible-function-pointer-types` and `int-conversion` (`stage2/batch-2.1/diagtool-groups-llvm20.txt`). + - Trial 1 (`batch-2.1/trial-fix.diff`) shows the effect. Three tests of T then read "gains a checked warning" instead of "does not compile", and 42 of the 45 pass: + - `check_const_suggestion_removal_reason`; + - the refuted case of `check_const_suggestion_trace`; + - `check_const_suggestion_cast_in_the_defining_file`. + - The policy therefore adds `-Werror=`, after those flags, for each tracked group whose diagnostics are all errors by default. Clang's `DiagnosticIDs` gives these levels (`getDiagnosticsInGroup`, `isDefaultMappingAsError`). With Clang 20, every default error of a tracked group belongs to such a group. + - Trial 2 (`batch-2.1/trial2-fix.diff`) passes the two new tests, and the 45 checks and fixtures of T. + - Cost if wrong: such a refutation would say "does not compile" where it should say "gains a checked warning". The verdict is the same. +2. **Review Focus 5 is a check, not a fixture variant** (Task 2.1). Validated by the user on 2026-10-22. Under `-Werror=`, the suggestion is removed both before and after the change. Only the reason shows that the group stayed a warning, and only the trace gives it. Cost if wrong: none. +3. **The type displayed is the plan's candidate** (Task 2.4). Revised as the user asked on 2026-10-22, then validated by the user with C's tag keywords kept. + - For `ReferenceRvaluePreferValue` and `PointerConstOnly`, once a suggestion has a candidate, the type displayed is written from `SuggestionPlan::candidate`. It replaces the rule's proposal, so no other definition of the proposed type is displayed. + - The planner writes it as Clang does, with `struct`, `union` and `enum` where C needs them. Only a type without a name takes the analyzer's label, ``, which Clang writes `(lambda)`: it reads better beside the rule's other lines. The trace keeps Clang's writing. + - Both rules take the same path. The D13 fixture, where the rule's proposal (`const int *p`) differs from the candidate, therefore pins that path for both. For `ReferenceRvaluePreferValue`, no fixture could: with the anonymous label, the rule's const reference and the candidate write the same text in every case found (`stage2/observations/rvalue-spelling/`). + - Cost if wrong: beside the rule's `current type` line, the displayed candidate can write a type otherwise, `struct foo` where the rule writes `foo`. +4. **Task 2.4 runs before Task 2.3.** Validated by the user on 2026-10-22. t3's suggestions are `PointerConstOnly` (`const IntPtr p` is `int *const p`), and Task 2.4 changes how that rule is displayed. In this order, the D10 fixtures are written once, with the candidate displayed. Cost if wrong: none. + +### Task 2.1: warning policy (D6, Review Focus 5) + +**Files:** `src/analysis/ConstSuggestionFacts.cpp` (`inspect`), `README.md`, `run_test.py`, `test/pointer_reference-const_correctness/const-suggestion-werror.c` (new). + +**Decision logic.** Both compilations, before and after the rewrite, go through `inspect`. For each tracked group, `-W` and `-Wno-error=`. Then `-Werror=` for each tracked group whose diagnostics are all errors by default (ruling 1). + +**Tests.** For D6 (t5), a new fixture: + +```c +// SPDX-License-Identifier: Apache-2.0 +// #217 (D6): the project turns its warnings into errors (-Werror) and disables a warning group +// that the check tracks, which this file triggers: strip returns a pointer to const as a plain +// pointer. The check keeps each tracked group a warning in both of its compilations: the file +// compiles as it is, and the suggestion for read_value is checked. + +// strict-expectation-details: true + +// at line 20, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_value' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// The project's compile command, which both the analysis and the check use. +// analyzer-arg: --compile-arg=-Werror +// analyzer-arg: --compile-arg=-Wno-incompatible-pointer-types-discards-qualifiers +// analyzer-arg: --experimental-const-suggestion-check + +int read_value(int* p) +{ + return *p; +} + +char* strip(const char* s) +{ + return s; +} +``` + +For Review Focus 5 (ruling 2), a new check, placed after `check_const_suggestion_trace` and registered after it: + +```python +def check_const_suggestion_werror_on_a_tracked_group() -> bool: + """ + #217 (Review Focus 5): a compile command that turns one tracked warning group into errors, + not every warning (-Werror=incompatible-pointer-types-discards-qualifiers), does not change + what the check sees: the group stays a warning in both compilations, and a rewrite that adds + such a warning is removed for that warning, not for an error. + """ + print("=== Testing a tracked warning group that the compile command turns into errors ===") + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_werror_") as tmp: + path = Path(tmp).resolve() / "keep.c" + path.write_text("void keep(int* p)\n{\n int* q = p;\n (void)q;\n}\n") + run = _const_suggestion_run( + [str(path), "--compile-arg=-Werror=incompatible-pointer-types-discards-qualifiers"] + ) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + removed = ( + f"keep parameter p: removed, gains a checked warning: {path}:3: initializing 'int *' " + "with an expression of type 'const int *' discards qualifiers" + ) + good = "keep" not in messages and any(line.startswith(removed) for line in trace) + print(f" {'✅' if good else '❌'} removed for the warning" + ("" if good else f": {trace}")) + print() + return good +``` + +Failure at `4fd0dae`, on macOS (`batch-2.1/red-rerun-4fd0dae-macos.txt`): +- **The fixture.** In both passes, default and smt-z3, the expected block is missing. The suggestion reads `not checked with Clang: the file does not compile with Clang as it is`. Only the two strict diagnostic counts pass, 2 of the 4 checks. +- **The check.** It reads `keep parameter p: removed, does not compile: …/keep.c:3: initializing 'int *' with an expression of type 'const int *' discards qualifiers`. + +With trial 2 applied, both pass, the fixture 4/4 (`batch-2.1/green-trial2-rerun-macos.txt`). + +- [ ] **Step 1: The fixture.** Add `const-suggestion-werror.c` and run it. Expected: FAIL in both passes, as above. + Commit: `test(const-param): check a suggestion despite -Werror and a disabled tracked group`. +- [ ] **Step 2: The check.** Add `check_const_suggestion_werror_on_a_tracked_group` and run it. Expected: FAIL, as above. + Commit: `test(const-param): a tracked group made an error stays a warning for the check`. +- [ ] **Step 3: The policy (ruling 1)**, in `inspect`, where the tracked groups are enabled: + ```cpp + // D6: each tracked group as Clang reports it by default, whatever the command + // says: enabled, a warning under -Werror, and an error when its diagnostics are + // errors by default (incompatible-function-pointer-types, int-conversion in C). + const clang::DiagnosticIDs& ids = *options.Diags->getDiagnosticIDs(); + std::vector errors; + for (const char* warning : kCheckedWarnings) + { + diagnostics.Warnings.push_back(warning); + diagnostics.Warnings.push_back(std::string("no-error=") + warning); + llvm::SmallVector members; + if (!ids.getDiagnosticsInGroup(clang::diag::Flavor::WarningOrError, warning, + members) && + std::all_of(members.begin(), members.end(), [&](clang::diag::kind id) + { return ids.isDefaultMappingAsError(id); })) + { + errors.push_back(std::string("error=") + warning); + } + } + diagnostics.Warnings.insert(diagnostics.Warnings.end(), errors.begin(), + errors.end()); + ``` + README: + - the judgement's step on errors and tracked warnings says that the tracked groups keep the level Clang gives them by default; + - the limit of a `-Werror` command that disables a tracked group goes. + + Run: both tests, then the tests of T. Expected: + - both tests PASS; + - `check_const_suggestion_removal_reason`, `check_const_suggestion_trace` and `check_const_suggestion_cast_in_the_defining_file` pass unchanged. + + Commit: `fix(const-param): keep the tracked warning groups as Clang reports them by default`. +- [ ] **Step 4: Task tests.** As Task 1.1, Step 6. + +### Task 2.2: each declaration's candidate, in its unit's language (D8, D18) + +**Files:** `src/analysis/ConstSuggestionInternal.hpp`, `src/analysis/ConstSuggestionFacts.cpp`, `src/analysis/ConstSuggestionCheck.cpp`, `run_test.py`. + +**Problem.** After the rewrite, step 3 compares each rewritten declaration with a single type: the plan's `expectedType`. That is the candidate of the defining declaration's parameter, as the defining unit writes it. +- **D8 (t4).** A C function takes a `struct foo *`, and a C++ file declares it through an `extern "C"` header. + - The defining unit writes the candidate `const struct foo *`; the C++ unit writes the same type `const foo *`. + - The C++ unit's declaration therefore "does not get the suggested type", and the suggestion is not checked. + - The C witness (`user2.c`) is verified. +- **D18.** A parameter's top-level qualifier is not part of the function's type, so its declarations may differ in it: `int f(int *p);` then `int f(int *const p) {…}`, or the other way round. + - The rewrite gives each declaration the candidate of its own type: `const int *p`, `const int *const p`. + - Only the defining declaration matches `expectedType`, so in both directions the suggestion is not checked (`stage2/observations/toplevel-qualifier/`). + +**Interfaces.** +- `UnitFacts::Entity::expected`, recorded in each compilation before the rewrite. For each declaration of the entity, by site, it holds the candidate of that declaration's own parameter type, top-level qualifiers included. It is written canonically in that unit's language (`writtenType` of `candidateOf`). + - The defining unit records it once it has planned the entity. + - The other units record it with the plan that their targets carry. +- `Target::expected`: the unit's `expected`, passed to its compilation after the rewrite. + +**Decision logic.** Step 3 compares each rewritten declaration with the expected type of its own site, in its unit. A declaration without one was another entity's before the rewrite, which merges into the entity: the collisions judge it (stage 1, ruling 3), as `check_const_suggestion_merged_declaration` and `check_const_suggestion_overload_defined_elsewhere` require. No comparison ignores the top-level qualifiers, as the user asked on 2026-10-22: the definition's own belong to its candidate, which D10 needs (`const IntPtr p` must become `const int *const p`). `SuggestionPlan::expectedType`, which only that comparison read, goes. + +**Tests.** Two new checks, placed after `check_const_suggestion_werror_on_a_tracked_group` and registered after it. They read the verdicts in the trace, which Task 2.4 does not change. + +```python +def check_const_suggestion_extern_c_user() -> bool: + """ + #217 (D8): a C function that a C++ file declares through a shared header (extern "C"). Each + file writes the parameter's type in its own language, struct foo * in C and foo * in C++, and + each rewritten declaration is compared with the candidate as its own file writes it: the + suggestion is checked with the C++ user, as with a C user. + """ + print('=== Testing a C function that a C++ file declares through an extern "C" header ===') + sources = { + "api.h": '#ifdef __cplusplus\nextern "C" {\n#endif\nstruct foo { int x; };\n' + "int rd(struct foo* f);\n#ifdef __cplusplus\n}\n#endif\n", + "def.c": '#include "api.h"\nint rd(struct foo* f) { return f->x; }\n', + "user.cpp": '#include "api.h"\nint user(foo* f) { return rd(f); }\n', + "user2.c": '#include "api.h"\nint user2(struct foo* f) { return rd(f); }\n', + } + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_extern_c_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + for label, user in (("a C++ user", "user.cpp"), ("a C user", "user2.c")): + run = _const_suggestion_run([str(root / "def.c"), str(root / user)]) + messages, trace, _ = run if run else ({}, [], "") + verdicts = [line for line in trace if line.startswith("rd parameter f, verdict: ")] + good = "rd" in messages and verdicts == ["rd parameter f, verdict: verified"] + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {verdicts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_top_level_qualifier() -> bool: + """ + #217 (D18): a parameter's top-level qualifier is not part of the function's type, so a + declaration and the definition can differ in it. The rewrite gives each declaration the + candidate of its own type, and each is compared with that candidate: in both directions, the + suggestion is checked. + """ + print("=== Testing declarations that differ from the definition in a top-level qualifier ===") + source = ( + "int f(int* p);\nint f(int* const p)\n{\n return *p;\n}\n" + "int g(int* const p);\nint g(int* p)\n{\n return *p;\n}\n" + ) + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_top_level_") as tmp: + path = Path(tmp).resolve() / "decls.c" + path.write_text(source) + run = _const_suggestion_run([str(path)]) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + ok = True + for label, function in ( + ("declared without the top-level const, defined with it", "f"), + ("declared with the top-level const, defined without it", "g"), + ): + verdicts = [line for line in trace if line.startswith(f"{function} parameter p, verdict: ")] + good = function in messages and verdicts == [f"{function} parameter p, verdict: verified"] + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {verdicts}")) + ok = ok and good + print() + return ok +``` + +Failure at `5b74b31`, on macOS (`batch-2.2/red-5b74b31-macos.txt`): + +```text +##### check_const_suggestion_extern_c_user: FAIL + ❌ a C++ user: ['rd parameter f, verdict: indeterminate: the rewritten declaration does not get the suggested type'] + ✅ a C user +##### check_const_suggestion_top_level_qualifier: FAIL + ❌ declared without the top-level const, defined with it: ['f parameter p, verdict: indeterminate: the rewritten declaration does not get the suggested type'] + ❌ declared with the top-level const, defined without it: ['g parameter p, verdict: indeterminate: the rewritten declaration does not get the suggested type'] +``` + +- [ ] **Step 1: The D8 check.** Add `check_const_suggestion_extern_c_user` and run it. Expected: FAIL for the C++ user, as above; the C user passes. + Commit: `test(const-param): check a C function that a C++ file declares through extern "C"`. +- [ ] **Step 2: The D18 check.** Add `check_const_suggestion_top_level_qualifier` and run it. Expected: FAIL for `f` and `g`, as above. + Commit: `test(const-param): check declarations that differ in a top-level qualifier`. +- [ ] **Step 3: Each declaration's expected type.** + - `candidateOf` is shared through `ConstSuggestionInternal.hpp`. + - `resolve` records `UnitFacts::Entity::expected` in each compilation before the rewrite. + - `checkUnit` passes the unit's own to its compilation after the rewrite, through `Target::expected`, and step 3 compares with it. + - README: the limit of a C function that C++ declares through an `extern "C"` header goes. + + Run: both checks, then the tests of T. Expected: PASS. + Before the task closes, as the user asked on 2026-10-22: + - t3's C and C++ cases (the review cases `t3/t.c` and `t3/t.cpp`) stay `not checked with Clang: the rewritten declaration does not get the suggested type`, until Task 2.3 corrects their rewrite; + - a mutant whose comparison ignores the top-level qualifiers makes them verified, so this replay detects it. + + Commit: `fix(const-param): expect each declaration's own candidate, in its unit's language`. +- [ ] **Step 4: Task tests.** As Task 1.1, Step 6. + +### Task 2.4: one candidate, displayed (D12, D13) + +**Files:** +- `src/analysis/ConstSuggestionPlan.cpp`: the candidate written for display, an anonymous type as ``; +- `src/analysis/ConstSuggestionCheck.cpp`: the type displayed, and the "checked" line; +- `src/analyzer/DiagnosticEmitter.cpp`: the advice that names the const reference alone; +- the fixtures `const-suggestion-deduction-kept.cpp` and `const-suggestion-deduction-refused.cpp` (changed), and one new fixture. + +**Decision logic.** Take each `ReferenceRvaluePreferValue` or `PointerConstOnly` suggestion that T keeps and whose plan has a candidate. Its displayed type becomes that candidate, written as ruling 3 says; the rule's own proposal is no longer displayed. +- **`ReferenceRvaluePreferValue` (D12).** The advice names the const reference alone. A verified suggestion's "checked" line says that passing by value is not checked (Texts). +- **`PointerConstOnly` (D13).** The advice and the "suggested type" line name the candidate, `const int *const p`. + +A suggestion without a candidate keeps the rule's proposal (§4). Without the option, nothing changes. + +**Tests (batch 2.4).** +- **D12, changed expectations:** + - the two `ReferenceRvaluePreferValue` blocks of `const-suggestion-deduction-kept.cpp`, advice and "checked" line; + - the block that D14 adds to `const-suggestion-deduction-refused.cpp`, advice only (stage 1, ruling 10). +- **D13.** A new fixture that enables T (t15: `int *const p`, only read), displayed and verified as `const int *const p`. +- The existing test without the option, and every other expectation of T, stay unchanged. + +Expected failures at the batch's base: the two alternatives and the old "checked" line (D12), and the displayed `const int *p` (D13). + +### Task 2.3: typedef with a qualifier (D10) + +**Files:** `src/analysis/ConstSuggestionPlan.cpp` (`Planner::plan`), new fixtures in C and C++. + +**Problem (t3).** In `const IntPtr p`, a pointer typedef carries a `const` written before it. +- The planner replaces only `IntPtr`, with `const int *`, which gives `const const int * p`: the pointer loses its own `const`. +- The candidate, `const int *const p`, is therefore not met, and the suggestion is not checked. + +**Decision logic.** +- The edit replaces the whole written type with the printed candidate of that declaration's type, `const int *const`. The written type runs from its first qualifier to the typedef's name, or to the last qualifier when one follows the name. The qualifiers written apart are thus folded into the candidate. +- A qualifier written by a macro leaves the plan indeterminate, with the new reason (Texts). +- A typedef without a separate qualifier (`IntPtr p`) keeps T v1's edit, `const int *`. + +**Tests (batch 2.3).** +- New fixtures in C and C++ (t3: `t.c`, `t.cpp`), verified with `const int *const p`. +- The C fixture also has a qualifier written by a macro, indeterminate with the new reason. + +Expected failure at the batch's base: in all three cases, `not checked with Clang: the rewritten declaration does not get the suggested type`. That failure also guards Task 2.2's comparison: one that ignored the top-level qualifiers would make t3 verified at the batch's base (`batch-2.2/mutant-ignore-top-level.diff`), and no other test of T detects it. + +**Validation of stage 2.** As for stage 1, for D6, D8, D10, D12, D13, D18 and Review Focus 5: +- their tests pass, and every other test of T passes unchanged; +- the suite passes on macOS, Linux arm64 and Linux x86_64; +- the review cases t3, t4, t5 and t15 give the verdicts of §6; +- the CI of #239 is green, and code scanning is triaged. + +## Stage 3 — measured costs, one Clang invocation + +### Task 3.1: measure +**Work:** the `const_suggestion.*` hotspots of `--timing` on the self-analysis with the option, as CI runs it; the per-suggestion copy of each unit's scan, the recompilation of a failing initial compilation, the duplicated construction of the invocation, each measured. +**Validation:** a measurement report in `evidence/217/v2-cost/`; only measurable costs go to Task 3.2. +**Status:** done (`report-fe43067.md`, `report-ct3-fe43067.md`). Measured on the self-analysis: the 38 compilations about 270 s (front-end about 166 s, inspection 87 s, CodeGen 17 s), the correspondences about 80 to 90 s, the facts' comparison 10 to 15 s, the scans 31 s; the copies of the scans 2.7 s; the preparation of the commands, the rewrite and the release of the modules under 1 s together. + +### Task 3.2: remove the measured costs; build the invocation once +**Work:** shared scans instead of copies; a failing initial compilation cached as failing; one function builds the Clang invocation for scanning and compiling, reusing the existing path normalization. +**Validation:** outputs identical before and after the stage (suite, review cases, corpora with the option); the gain measured. +**Status:** partly done. The correspondences' index is built once per call (O1, `48f3ec1`): correspond 49.5 -> 23.0 s, same pairs and rounds. The shared scans and the single construction of the invocation are not done: their measured cost (2.7 s, and under 1 s) is below the noise. The user deferred the single construction of the Clang invocation to a separate follow-up (2026-10-26), the task staying partly done; that deferral does not accept the cost overrun. The failing initial compilation is not cached either: 4 such compilations on the self-analysis. The faster fact comparison (O2) was withdrawn by the user; the precompiled preamble (O3) was abandoned after its prototype (a net loss, a crash); the reduced scans (O4) and the stop after an introduced error (O5) were not engaged; sharing the pipeline's compilation (O6) is not recommended (`final/o6-feasibility.md`). The +10 % criterion is not met. + +## Final phase + +### Task F.1: corpora +**Work:** on Linux, `src/`, zlib, tinyxml2 and PCRE2, three outputs: without the option, T v1, T v2. Each suggestion classified verified, kept indeterminate, or removed on an established refutation. Each disappearance against the output without the option is explained by its violation; each difference with T v1 is explained. +**Status:** done at `48f3ec1` (`final/report.md`); the seven differences with T v1 led to Tasks F.4 and F.5, each of which changed only the cases it targets (src/ now 10 verified, 0 not checked, 12 removed; zlib 16/0/10, tinyxml2 0/0/6, PCRE2 10/31/3 with the corpora configured in the container). **Reserve (#215):** every comparison leaves out the units on which the analysis does not finish, as T v1's measurements did: 5 of zlib's 37 (`crc32.c`, `deflate.c`, `infback.c`, `inffast.c`, `inflate.c`) and 14 of PCRE2's 35 (`pcre2_auto_possess.c`, `pcre2_compile.c`, `pcre2_compile_class.c`, `pcre2_convert.c`, `pcre2_dfa_match.c`, `pcre2_find_bracket.c`, `pcre2_match.c`, `pcre2_pattern_info.c`, `pcre2_script_run.c`, `pcre2_study.c`, `pcre2_valid_utf.c`, `pcre2_xclass.c`, `pcre2grep.c`, `pcre2test.c`). Analyzed alone without the option by the product binary, each runs past a 120 s limit (2026-10-26). The suggestions of these units are not classified; the conclusions on the corpora hold for the units analyzed only. + +### Task F.2: cost +**Work:** the protocol of §8. The criterion is +10 % on the self-analysis. +**Status:** measured, not met: +24 to +25 % in a 6 GiB container, +30 to +32 % in a 10 GiB one (4 CPUs, Linux x86_64); T v1 measured +14.6 % in the first conditions. No overrun is accepted. + +### Task F.3: delivery +**Work:** README; the description of the draft #239, opened after stage 1, brought up to date; CI; code scanning triaged; final review on the updated base. No merge. +**Status:** in progress. The integrated head is published on #239, still a draft; README, description, CI and code scanning are done at `bed265d`. The final review on the updated base is still due: its findings, and their fixes, come before any decision to merge. + +### Task F.4: the scope of step 3, written form and deduced component (D19) +Found by the comparison of the corpora at `48f3ec1`, authorized by the user on 2026-10-23. +**Work:** +- 1a: a declaration whose type has a deduced part (`auto`, `decltype(auto)`, `decltype`, `typeof`, a deduced class template's arguments) is recorded by its written form, the deduced part written as its keyword; any other declaration as before, canonically. +- 1b: in each compilation after the rewrite, a type location whose spelling an edit writes, and that does not belong to a planned parameter's type, is outside the plan: "the rewrite also changes the type written at :", reported after the declarations. A planned parameter's default argument is outside its type. +- The order of the judgement's steps is unchanged; the checks of D5, of the X-macro and of the invalid declarations stay as they are. + +**Tests:** `check_const_suggestion_deduced_type_error` (publish_all removed for its error; FAIL at `48f3ec1`), `check_const_suggestion_written_part_outside_the_plan` (a written template argument beside a deduced one, a cast in a deduced declaration's initializer; PASS at `48f3ec1`, FAIL with a fix that ignores declarations with a deduced part). A check of a planned parameter's default argument is presented with its proof before it is added. + +**Validation:** the three platforms; the comparison of the corpora, where only publishSingleStoreSlots may change (removed for its error); the cost added. + +### Task F.5: the correspondences of nested entities (§5.7, step 5; D20) +Found by the comparison of the corpora at `48f3ec1`; a throwaway prototype, then its implicit-member extension, measured before the integration, which the user authorized on 2026-10-25. +**Work:** identities read before the AST goes (members of nested types, specializations whose arguments or type hold one, implicit members of classes whose key holds one); step 5 maps their nested types through the pairs, keeps every other part of a key, pairs one to one and refuses ambiguities, in a fixpoint common with steps 3 and 4. +**Tests:** end-to-end checks in run_test.py first (five FAIL at `5167998`, the negative one passes there), then the unit tests of the keys and of `correspond`, which need the integration's declarations. Each was proven against mutants of the prototype; the kind and the name of an implicit member, like the name, static flag and qualifiers of a nested member, are protected only together (§5.7). +**Validation:** the three platforms with the full suite; the corpora, where only parallelFor x2, build x2 and isRecursiveCallee x2 may change (checked). + +**Validation of the final phase:** every classification explained; cost reported against the criterion; CI green; triage summarized. diff --git a/docs/superpowers/specs/2026-10-16-const-suggestion-check-v2-design.md b/docs/superpowers/specs/2026-10-16-const-suggestion-check-v2-design.md new file mode 100644 index 0000000..145825a --- /dev/null +++ b/docs/superpowers/specs/2026-10-16-const-suggestion-check-v2-design.md @@ -0,0 +1,405 @@ +# Vérification des suggestions const par Clang, version 2 (#217) + +Statut : validée à `8b283a8`, puis complétée à la relecture du plan d'implémentation (D17, fixture +de D13), au détail de l'étape 1 (mutualisation limitée au module, §5 et §10), à celui de l'étape 2 (erreurs natives de Clang conservées, §5.3 ; D18, §5.6 et §6), après la comparaison des corpus (D19, §5.6 et §6), puis à l'intégration de l'étape 5 de §5.7 (D20). N'autorise ni le code ni la modification des tests : chaque lot présentera ses +tests, avec leurs preuves d'échec, avant de les ajouter. La fusion n'est pas autorisée. +Issue : #217. Liens : #221 (T v1, référence conservée), #167 (faits et contrat const), #197 +(identité d'un global entre fichiers). +Références au code : `2a5b0c1`, c'est-à-dire #221 avec `main` b80d127 fusionné. Bilan et preuves : +`coretrace-stack-analyzer-evidence/217/` (`bilan-final-2a5b0c1.md`, `final-review/`, `optin-cost/`). + +## 1. Constat + +T v1, derrière `--experimental-const-suggestion-check`, vérifie avec Clang chaque suggestion de +`ConstParameterNotModified` : il réécrit en mémoire les déclarations du paramètre, recompile les +fichiers qui voient le changement et compare leurs diagnostics et leur code généré. + +Sur `src/`, zlib, tinyxml2 et PCRE2 (même binaire, Linux x86_64), 97 suggestions : 36 vérifiées, +31 gardées non vérifiées, 30 retirées, aucune par un défaut de T. L'auto-analyse coûte +11,3 % avec +l'option, pour un objectif de +10 %. + +La revue finale a confirmé dix défauts à `2a5b0c1` (§6), dont deux suggestions fausses affichées +comme vérifiées. Leurs causes sont communes : +1. **Identité approchée.** La fonction est retrouvée par son symbole avant la réécriture, puis après + par ses noms décodés (`withoutConst`, `variantOf`), sans sa liaison et sans contrôle + d'ambiguïté : le dernier candidat trouvé l'emporte. +2. **Paramètre.** Son indice source est calculé `debugArgNo − 1 − this` : un paramètre artificiel + (`vtt`) le décale. La règle elle-même reçoit de `ParameterDebugBinding` un `vtt` typé comme le + premier paramètre utilisateur. +3. **Plan sans contrôle de portée.** Une modification peut atteindre un autre paramètre (argument de + macro réutilisé) ou d'autres fonctions (corps de macro mal reconnu). +4. **Retrait sur une différence non expliquée.** « Le code généré change » retire la suggestion, + même quand rien n'établit une violation. +5. **Unité réduite au fichier.** Une seule commande par fichier ; le type attendu est comparé en + texte, dans la langue du fichier de définition. +6. **Candidat affiché différent du candidat vérifié** pour `PointerConstOnly` et + `ReferenceRvaluePreferValue`. + +## 2. Objectif et périmètre + +**Objectif.** Chaque verdict est justifié et rattaché au candidat exact, aux unités et aux +configurations effectivement vérifiées. + +**Dans le périmètre (refonte interne de T) :** +- identités explicites, plan séparé de son exécution, unité définie par un fichier et une + configuration, faits R, O et K, correspondances établies, collisions, verdicts à trois états, + trace ; +- étape 2 : D6, D8, D10, D12, D13 et D18 (§6) ; +- étape 3 : les coûts évitables mesurés et une seule construction de l'invocation Clang. + +**Hors périmètre :** +- une compilation commune avec coretrace-compiler, un cache persistant ; +- la refonte du comparateur, qui ne reçoit qu'une entrée nouvelle (§5.9) ; +- les configurations multiples par fichier et `ParameterDebugBinding` (P0) : deux changements + séparés, P0 étant un préalable ; +- des groupes d'édition de liens fournis par la construction ; +- le comportement sans l'option, inchangé. + +**Conservé de T v1 :** la séparation entre l'analyse LLVM et la vérification Clang, la réécriture en +mémoire, la prise en compte des autres fichiers, les normalisations et les exceptions acceptées du +comparateur, les tests. + +## 3. Définitions + +- **Suggestion** : un `ConstParamIssue` : symbole de la fonction, argument, règle, + `reliesOnCalleeConst`. +- **Candidat** : le type exact proposé pour un paramètre source et l'alternative qu'il représente, + construit une seule fois par le planificateur à partir des types de Clang : + + | Règle | Candidat | + |---|---| + | `Pointer` | pointé const : `const T *p` | + | `PointerConstOnly` | pointé const, const de tête conservé : `const T *const p` | + | `Reference` | `const T &r` | + | `ReferenceRvaluePreferValue` | l'alternative référence constante `const T &r` ; le passage par valeur n'est pas vérifié | + +- **Unité** : un fichier et une configuration, identifiée par l'empreinte de sa commande normalisée + (arguments et répertoire de travail). Le chargement actuel fournit au plus une configuration par + fichier ; T n'en revendique aucune autre. +- **Identités** : + - *source* : l'entité (fonction, méthode, constructeur, opérateur…), sa liaison (externe, + interne, aucune), ses redéclarations (fichier réel, position) et son origine de gabarit ; + - *paramètre* : son indice dans la liste des paramètres source, et le `ParmVarDecl` de chaque + redéclaration ; + - *ABI* : les symboles que CodeGen émet pour chaque variante (`GlobalDecl`), avant et après. +- **Faits R, O, K** : §5.4. +- **Correspondance établie** : un appariement déduit de faits de compilation (identité source, plan, + contextes d'instanciation), jamais d'un nom décodé. +- **Verdicts** : + - *vérifiée* : dans chaque unité vérifiée, aucune violation, toutes les correspondances + établies, et un code égal aux exceptions acceptées près ; + - *réfutée* : une violation du contrat est établie : erreur de compilation introduite (une fois + la portée validée), avertissement suivi introduit, ou fait R, O ou K changé ; + - *indéterminée* : identité ambiguë, correspondance manquante ou seulement supposée, unité non + vérifiable, portée inconnue ou non respectée, comparaison non concluante, conversion explicite + de l'adresse sans violation établie, collision possible entre fichiers. + +## 4. Sortie et trace + +- **Vérifiée** : la suggestion est gardée. Le type proposé affiché est le candidat (D12, D13). La + ligne « checked … » nomme le candidat, son alternative, la portée (fichiers analysés ou base de + compilation) et les configurations vérifiées, puis les différences acceptées et la ligne ABI. +- **Indéterminée** : gardée, avec « not checked with Clang: ». Le type affiché est le + candidat s'il existe, sinon la proposition de la règle. +- **Réfutée** : retirée des diagnostics publics. +- **Trace** (`--verbose`), pour chaque verdict : la suggestion (règle, fonction et symbole, + paramètre par son nom et son indice source), la transformation (fichier et ligne, type d'origine + et candidat, alternative), l'unité et sa configuration, le verdict et sa raison. +- **Sans l'option** : aucun changement. +- **#167** : les lignes liées à `reliesOnCalleeConst` restent inchangées. Un verdict de T porte sur + le changement de type ; il ne vaut jamais preuve d'absence d'écriture. + +## 5. Architecture + +Les responsabilités ci-dessous sont séparées dans le code, sans qu'aucune n'exige sa propre classe +ou interface. + +**Flux.** +1. Compilation initiale du fichier de définition, et collecte de ses faits « avant ». +2. Planification de chaque suggestion sur cette même compilation. +3. Compilation initiale de chaque autre unité concernée, une fois par unité et configuration, + réutilisée pour toutes les suggestions du même module analysé. À l'étape 1, la mutualisation + s'arrête là : T s'exécute une fois par module, et une unité qui voit les changements de deux + modules est compilée telle quelle une fois pour chacun. C'est moins que la mutualisation prévue + d'abord, pour toutes les suggestions de l'analyse (§10). +4. Compilation modifiée de chaque unité concernée. +5. Correspondance et jugement par unité, agrégation, rapport. + +### 5.1 Prise en charge + +Une suggestion devient : le paramètre source (indice fourni par P0, ou « artificiel » ou +« inconnu »), la règle et l'alternative attendue. + +### 5.2 Unités + +Le fichier de définition (avec la commande de l'entrée analysée), les autres entrées analysées et, +avec `--compile-commands`, les fichiers C et C++ de la base. Une unité par fichier et par +configuration fournie. Une unité sans commande, impossible à prétraiter ou qui ne compile pas telle +quelle est non vérifiable. Une unité voit le changement quand elle lit un fichier réécrit, quand son +code analysé référence l'entité (en tenant compte de la liaison), ou, pour une fonction libre à +liaison externe, quand elle en nomme l'identifiant après prétraitement. + +### 5.3 Compilation initiale et faits « avant » + +Une compilation par unité et configuration ; pour le fichier de définition, la même compilation +sert au plan. Elle relève : +- les déclarations de l'entité ; +- les faits R, O et K ; +- les symboles définis et leur liaison ; +- les conversions explicites de l'adresse de la fonction ; +- les erreurs et les avertissements suivis ; +- l'IR préparé pour le comparateur. + +Les groupes d'avertissements suivis sont activés explicitement par `-W`, puis maintenus comme +avertissements par `-Wno-error=`, avec la même politique dans les compilations initiale et +modifiée. Les erreurs natives de Clang restent des erreurs. `-Wno-error=` rétrograderait +aussi les diagnostics que Clang traite comme des erreurs par défaut. Chaque groupe suivi dont tous +les diagnostics sont des erreurs par défaut, d'après les niveaux que Clang leur donne, est donc +ensuite remis en erreur par `-Werror=`. Avec Clang 20, ce sont, en C, +`incompatible-function-pointer-types` et `int-conversion`, et chaque erreur par défaut d'un groupe +suivi appartient à l'un d'eux. Une commande de projet qui désactive un groupe (`-Wno-`), ou +qui promeut en erreurs les avertissements (`-Werror`) ou un seul groupe (`-Werror=`), ne +change donc ni leur relevé ni leur nature (D6). + +### 5.4 Faits R, O et K + +Une liste fermée : +- **R, référence** : la déclaration que vise un appel ou une référence : fonctions, spécialisations, + constructeurs, opérateurs, conversions ; un appel virtuel par sa cible statique. Les opérandes non + évalués (`sizeof`, `decltype`) et les instanciations sont inclus. +- **O, override** : pour chaque méthode qui porte le nom de l'entité, les méthodes qu'elle surcharge. +- **K, constante** : la valeur de chaque condition `if constexpr`, et l'association que choisit + `_Generic`. + +Rien d'autre n'est relevé : ni le type des expressions, ni les conversions implicites, ni les autres +constantes. Une référence à l'entité réécrite doit viser la même entité avant et après, selon le +plan. + +**Clé d'un fait** : l'unité ; la chaîne des contextes d'instanciation (gabarit et arguments) ; la +position source, à la fois d'écriture et d'expansion, ramenée à l'original par les modifications du +plan dans les fichiers réécrits. + +### 5.5 Planification + +Sur la compilation initiale du fichier de définition : +- **Entité** : localisée par le symbole de la suggestion (CodeGen), puis décrite par son identité + source. +- **Paramètre** : indice source fourni par P0. Un paramètre artificiel ou introuvable donne + « indéterminée ». +- **Candidat** : calculé sur le type structuré (§3). +- **Modifications**, sur le `TypeLoc` de chaque redéclaration : + - un type écrit dans un argument de macro est réécrit là où il est écrit ; + - un type écrit par une macro qui n'écrit que ce paramètre reçoit `const` devant l'invocation ; + - une définition de macro, reconnue par le préprocesseur et non par le texte (D11), n'est jamais + réécrite : indéterminée ; + - un typedef de pointeur reçoit le candidat imprimé, et un qualificatif écrit à part est traité + (D10) ; sinon, indéterminée. +- **Portée du plan** : chaque modification ne sert que les paramètres prévus. Aucun autre paramètre + ni aucune autre déclaration ne s'écrit à la même position (D5). Sinon, indéterminée. + +### 5.6 Compilation modifiée et inspection « après » + +Les fichiers réécrits sont remappés ; les mêmes faits qu'en 5.3 sont relevés. La portée est vérifiée +dans chaque unité : +- aucune déclaration écrite dans les sources des fichiers réécrits ne change hors du plan dans sa + forme écrite : ses qualificatifs, ses déclarateurs, les types et les arguments de gabarit que le + source écrit. Sa composante déduite (`auto`, `decltype`, arguments déduits d'un gabarit de + classe) n'en fait pas partie : ce que la réécriture y change est une conséquence du plan, que + jugent les étapes 4 à 7 (D19). Les signatures instanciées (spécialisations de gabarits) qui changent avec la réécriture ne relèvent + pas de cet invariant : elles sont examinées par les correspondances (5.7), comme dans + `const-suggestion-deduction-kept.cpp` ; +- aucun type qu'une modification écrit ne sert hors du type des paramètres prévus, dans une + déclaration comme dans une expression (conversion, `sizeof`, argument de gabarit) ; l'argument + par défaut d'un paramètre prévu est hors du plan (D19) ; +- chaque redéclaration visée a le candidat de son propre type, qualificatifs de premier niveau + compris (D18), comparé dans la langue de son unité (D8). + +Si les déclarations nécessaires n'ont pas pu être inspectées (erreur fatale, unité incomplète), la +portée est inconnue. + +### 5.7 Correspondance + +Par unité, entre ses compilations initiale et modifiée, jamais entre deux unités ni entre deux +configurations : +- **Symboles de l'entité** : chaque variante, par `GlobalDecl`. +- **Constructeurs hérités** : il faut savoir quels symboles émis, avant et après, sont les variantes + héritées du constructeur visé. Technique candidate : les constructeurs implicites qui en héritent + (`getInheritedConstructor()`), et leurs symboles par CodeGen. Elle doit être démontrée (cas `t8` + et un cas avec gabarit) avant d'être adoptée ; sans démonstration, ces cas restent indéterminés. +- **Entités imbriquées dans la cible** : lambdas, statiques locales, classes locales et noms RTTI. +- **Contextes d'instanciation dont les arguments changent** : appariés seulement si les faits qui + les demandent se correspondent (chacun avec son propre contexte), visent le même gabarit avant et + après, désignent tous la même paire, et forment une bijection. Sinon, indéterminée, et la raison + nomme le contexte. +- **Entités imbriquées dans une paire établie** (étape 5, P2) : les types imbriqués (lambdas, classes + locales) de toute fonction déjà appariée, et ce qu'ils atteignent, identifiés par les faits de + Clang lus avant la destruction de l'AST, jamais par des heuristiques de noms : + - un membre d'un type imbriqué, par sa nature, sa variante ABI, les positions du type et du + membre, son nom, `static`, ses qualificatifs et son type ; + - une spécialisation dont les arguments ou le type contiennent un type imbriqué, par sa variante, + la position de son patron, les arguments des spécialisations englobantes, les siens et son type + de fonction ; + - un membre implicite d'une classe dont la clé contient un type imbriqué, par cette classe, sa + nature et son nom, sa variante et son type complet, qualificatifs compris. + + Dans une clé, seuls les types imbriqués passent par les paires ; tout le reste (autres arguments, + qualifications, type) est comparé tel quel. Seuls les symboles que la réécriture renomme y + prennent part. Les étapes 3, 4 et 5 tournent jusqu'à un point fixe commun. Une clé portée par plus + d'un symbole d'un côté ne donne aucune paire, et une raison. Une spécialisation dont la + réécriture change elle-même le type ou un argument reste sans paire. Les globales (vtables, + typeinfo, constantes `__const`) ne sont pas couvertes. Limite des tests : la nature et le nom d'un + membre implicite ne sont pas montrés nécessaires (sur les cas réels, la variante ou le type + suffit à séparer), de même que chacun des nom, `static` et qualificatifs d'un membre de type + imbriqué ; ils sont gardés et protégés ensemble. + +### 5.8 Jugement + +Par unité, dans cet ordre : +1. unité non vérifiable : indéterminée ; +2. inspection « après » incomplète, donc portée inconnue : indéterminée ; +3. portée du plan non respectée : indéterminée ; +4. erreur de compilation introduite : réfutée ; +5. avertissement suivi introduit : réfutée ; +6. fait R, O ou K changé, selon des correspondances établies : réfutée ; +7. comparaison de l'IR avec les paires établies : égalité aux exceptions acceptées près, vérifiée ; + différence que les faits n'expliquent pas, symbole non apparié ou paire seulement supposée, + indéterminée. + +Une conversion explicite de l'adresse est relevée et nommée ; seule, elle donne « indéterminée ». + +### 5.9 Comparateur + +Inchangé, à une entrée près : il reçoit les paires établies (5.7). +- Ses heuristiques (encodage masqué, `withoutConst`, `variantOf`, clé de gabarit) ne servent plus + qu'à situer une différence dans la trace, pour les symboles que ces paires ne couvrent pas. +- « Vérifiée » exige que chaque symbole qui disparaît ou apparaît soit couvert par une paire + établie. Une paire seulement supposée donne au mieux « indéterminée ». +- Une heuristique n'a aucun pouvoir de verdict : elle ne peut ni établir une paire ni en invalider + une. Seuls des faits insuffisants ou incohérents empêchent d'établir une correspondance ; une + approximation de nom n'invalide jamais une preuve. +- Les exceptions acceptées restent celles de T v1, chacune vérifiée et listée : le nom que `typeid` + donne à un type nommé d'après la fonction, et la signature imprimée par `__PRETTY_FUNCTION__`. + +### 5.10 Collisions, édition de liens et agrégation + +**Collisions.** Après la réécriture, un symbole que prend l'entité est-il défini par une autre +entité ? Une même entité définie dans plusieurs fichiers, comme une fonction `inline` de l'en-tête +réécrit, n'est pas une collision. + +| Liaison de l'autre définition | Règle à l'édition de liens | Verdict | +|---|---|---| +| locale (`static`, espace anonyme) | aucune portée entre fichiers | jamais de collision | +| forte, dans le même fichier | redéfinition | erreur de compilation, réfutée à l'étape 4 | +| forte, dans un autre fichier | définition multiple si les deux fichiers sont liés ensemble | indéterminée | +| `weak` | pas d'erreur ; la définition forte l'emporte en silence | indéterminée, en nommant laquelle l'emporterait | +| `weak_odr`, `linkonce_odr` | fusion sous promesse ODR | indéterminée | + +Conclure sur l'ODR exigerait l'équivalence des définitions source : T ne l'établit pas, et l'IR ne +tranche pas, ni par son égalité ni par sa différence. + +**Déclaration périmée.** Une unité qui déclare l'entité avec l'ancien type sans voir la réécriture +rend la suggestion indéterminée. + +**Hypothèse d'édition de liens.** T ne déduit jamais que des fichiers forment un même programme. +Toute raison qui en dépend énonce l'hypothèse et sa portée, par exemple : « `_Z1fPKi` est aussi +défini dans b.cpp ; si a.cpp et b.cpp sont liés ensemble, les deux définitions entrent en +collision ». + +**Agrégation.** La suggestion est réfutée si une unité la réfute ; sinon indéterminée si une unité ou +une collision l'est ; sinon vérifiée, dans les unités et configurations vérifiées. + +### 5.11 Rapport + +Selon §4. + +## 6. Défauts confirmés + +Cas dans `coretrace-stack-analyzer-evidence/217/final-review/cases/`, observés à `2a5b0c1`. « VP » : +suggestion valide. + +| # | Défaut (cas) | Fait observé | Verdict attendu | Test prévu | Étape | +|---|---|---|---|---|---| +| D1 | paramètre artificiel `vtt` d'un constructeur de base (t10) | la règle suggère `vtt` ; T réécrit `x` et l'affiche « checked » ; `x` (variante C2) est retiré | P0 : plus de suggestion sur `vtt`. T : `x` vérifiée. Un paramètre artificiel qui atteindrait T : indéterminée | P0 : test unitaire de la liaison et fixture de la règle. T : fixture du constructeur | P0, 1 | +| D2 | opérateur membre utilisé dans un autre fichier (t1) | retirée : « the methods it overrides change » | vérifiée (VP) | contrôle run_test.py à deux fichiers | 1 | +| D3 | homonymes C `static` et externe (t2, deux sens) | retirée : « declared in b.c / d.c » | vérifiée (VP), dans les deux sens ; README corrigé | contrôle run_test.py, deux sens | 1 | +| D4 | `f(const int*)` défini dans un autre fichier (t9) | « checked in 2 file(s) » ; l'appliquer définit `_Z1fPKi` deux fois | indéterminée, en énonçant l'hypothèse d'édition de liens | contrôle run_test.py : fichiers analysés ensemble, et autre fichier venant seulement de la base | 1 | +| D5 | type écrit dans un argument de macro qui sert deux paramètres (t3/m.c) | retirée : erreur sur `dst` | indéterminée (VP gardée) : type écrit une seule fois pour `src` et `dst` | fixture | 1 | +| D7 | constructeur hérité (t8) | retirée : symboles `CI1`/`CI2` non appariés | vérifiée si 5.7 est démontrée, sinon indéterminée ; jamais retirée | fixture | 1 | +| D11 | `\` suivi d'espaces dans un `#define` (t13) | corps de macro réécrit, puis retirée | indéterminée (VP gardée) : déclaration écrite par une définition de macro | fixture | 1 | +| D14 | correspondance non unique traitée comme une réfutation (`const-suggestion-deduction-refused.cpp`, cas `call`) | retirée | indéterminée (gardée) ; `pick` reste réfutée (condition `if constexpr`) | attente modifiée de la fixture | 1 | +| D15 | conversion explicite de l'adresse traitée comme une réfutation (`cross-tu-const-suggestion-cast.c`) | retirée | indéterminée (gardée), sauf erreur ou effet établi | attentes modifiées des deux contrôles run_test.py qui l'utilisent | 1 | +| D16 | `sizeof(__PRETTY_FUNCTION__)` (`const-suggestion-printed-signature.c`, cas `signature_size`) | retirée : le code généré change | indéterminée (gardée) : la différence n'est expliquée ni par R, ni par O, ni par K ; K n'est pas élargi pour conserver l'ancien verdict | attente modifiée de la fixture | 1 | +| D17 | redéclaration avec l'ancien type dans un autre fichier (`cross-tu-const-suggestion-redecl.c`), relevée à la planification | retirée : le fichier déclare la fonction lui-même, sans l'en-tête | indéterminée (gardée non vérifiée, ce qui ne démontre pas qu'elle est valide) ; la raison nomme le fichier et l'hypothèse d'édition de liens (§5.10) | attentes modifiées de `check_const_suggestion_cross_tu` et de `check_const_suggestion_compilation_database` | 1 | +| D6 | `-Werror` du projet et groupe suivi désactivé (t5) | « not checked: does not compile as it is » | vérifiée (VP) | fixture avec `--compile-arg` | 2 | +| D8 | définition C, utilisateur C++ via `extern "C"` (t4) | « not checked: the rewritten declaration does not get the suggested type » ; témoin C vérifié | vérifiée (VP) | contrôle run_test.py, avec le témoin | 2 | +| D10 | `const` écrit devant un typedef de pointeur (t3/t.c, t3/t.cpp) | réécrit en `const const int *` ; « not checked » | vérifiée (`const int *const`) ; indéterminée si le qualificatif vient d'une macro | fixtures C et C++ | 2 | +| D12 | alternative de `ReferenceRvaluePreferValue` | seul `const T &` est vérifié ; la ligne dit « the suggested type » | vérifiée ; le type affiché et la ligne nomment `const T &`, le passage par valeur n'est pas vérifié | attentes modifiées (`const-suggestion-deduction-kept.cpp` et autres) | 2 | +| D13 | `PointerConstOnly` (t15) | affiche `const int *p`, vérifie `const int *const p` | vérifiée ; le type affiché et la ligne nomment `const int *const p` | nouvelle fixture qui active T, aucune fixture de T v1 ne couvrant `PointerConstOnly` ; le test existant sans l'option reste inchangé | 2 | +| D18 | redéclaration qui diffère de la définition par un qualificatif de premier niveau du paramètre (`int f(int *p);` puis `int f(int *const p) {…}`, et l'inverse), observée à `4fd0dae` (`stage2/observations/toplevel-qualifier/`) | « not checked: the rewritten declaration does not get the suggested type » | vérifiée (VP), dans les deux sens | contrôle run_test.py, deux sens | 2 | +| D19 | déclaration dont le type est déduit d'un appel que la réécriture rend invalide (`publishSingleStoreSlots`, `IntRanges.cpp:314`, relevée par la comparaison des corpus à `48f3ec1`) | gardée : « not checked: the rewrite also changes the type of an unnamed declaration » | réfutée pour l'erreur introduite ; une partie écrite que la réécriture change hors du plan, y compris dans une expression ou un argument par défaut, reste indéterminée | contrôles run_test.py | F | +| D20 | spécialisations et membres implicites qui dépendent d'entités imbriquées dans une autre paire (`parallelFor`, `build`, `isRecursiveCallee`, corpus à `48f3ec1` ; P2a, P2b, P2c) | gardées : « not checked: the generated code changes … symbols no longer used » | appariées par l'étape 5 de 5.7, vérifiées si l'IR concorde ; une clé ambiguë, ou dont la réécriture change une autre partie, reste sans paire et indéterminée | tests unitaires et contrôles run_test.py | F | + +**Non retenus :** +- le cache « avant » indexé par l'identifiant (point 9 de la revue) : non reproduit (t12) ; les + caches de T v2 sont indexés par entité et par configuration ; +- `typedef int *const CP` (t14) : la règle n'y fait aucune suggestion. + +**Étape 3, sans changement de comportement :** la copie du scan de chaque unité pour chaque +suggestion, l'absence de cache pour une compilation « avant » qui échoue, et la construction de +l'invocation Clang en double. Les deux coûts ne sont traités que s'ils sont mesurables. + +## 7. Tests + +- **TDD, défaut par défaut.** Chaque test est soumis avec la preuve de son échec avant d'être ajouté. +- **Non-régression.** Tous les tests de T v1, sauf les attentes à modifier, elles aussi soumises : + D12, D14, D15, D16 et D17. D13 ajoute une fixture qui active T ; le test existant sans l'option + reste inchangé. +- **Corpus** (`src/`, zlib, tinyxml2, PCRE2). Chaque suggestion est comparée entre trois sorties : + sans l'option, T v1 et T v2. Elle est classée vérifiée, gardée indéterminée, ou retirée sur une + réfutation établie. Chaque disparition par rapport à la sortie sans l'option est expliquée par sa + violation, et chaque écart avec T v1 l'est aussi. T v1 n'est pas une référence de justesse. +- **Plateformes.** macOS, Linux (WSL), puis la CI avec le tri du code scanning. + +## 8. Coût + +Le même binaire, sans puis avec l'option, en alternance (sans, avec, avec, sans), sur la même base et +les mêmes configurations : +- l'auto-analyse telle que la CI la lance (4 tranches, 2 à la fois, profil rapide, caches froids) ; +- zlib, tinyxml2 et PCRE2. + +Le critère reste +10 % sur l'auto-analyse. La validation de cette architecture ne vaut pas +acceptation d'un dépassement. + +## 9. Livraison + +1. **P0, `ParameterDebugBinding`** : issue et PR propres, depuis `main`, avec leurs tests. Préalable + à l'étape 1. +2. **Branche `217-t-v2`**, depuis `2a5b0c1`. T v1 reste une référence immuable (`217-t-v1`, #221, + bundle). Après la fusion de P0, `main` actualisé y est intégré et l'ensemble est validé. +3. **Étape 1** : flux, identités, plan et portée, faits R, O et K, correspondances, paires établies + pour le comparateur, collisions, trace ; D1 (partie T), D2, D3, D4, D5, D7, D11, D14, D15, D16 + et D17. +4. **Étape 2** : D6, D8, D10, D12, D13 et D18. +5. **Étape 3** : les coûts mesurés et l'invocation Clang construite en un seul endroit. +6. **Comparaison des corpus, mesure du coût et README** ; une PR en brouillon. La revue finale porte + sur la base actualisée. + +Chaque étape est validée (tests, plateformes, CI) avant la suivante. Les configurations multiples +restent un changement séparé, hors de ce calendrier. + +## 10. Limites + +- Sans groupes d'édition de liens, une collision entre fichiers reste indéterminée. +- L'ODR n'est pas tranchée. +- Seules les configurations fournies par le chargement sont vérifiées. +- Une suggestion qui change réellement le comportement, d'une façon que R, O et K ne couvrent pas, + reste affichée comme non vérifiée, sans être retirée : par exemple `signature_size`, où + `sizeof(__PRETTY_FUNCTION__)` change. C'est la conséquence de la politique choisie : une + différence d'IR inexpliquée ne suffit plus à retirer une suggestion. +- La technique d'identification des constructeurs hérités reste à démontrer. +- Un succès du cache IR ne fournit pas d'AST : T compile toujours depuis les sources. +- La compilation initiale d'une unité n'est partagée qu'entre les suggestions d'un même module + (§5, étape 3) : la partager entre modules demanderait les faits d'entités pas encore planifiées. + Ce compromis, accepté provisoirement pour l'étape 1, réduit la mutualisation prévue ; son coût + entre modules est mesuré à l'étape 3 (tâche 3.1). Le critère de +10 % sur l'auto-analyse (§8) + reste inchangé. diff --git a/include/StackUsageAnalyzer.hpp b/include/StackUsageAnalyzer.hpp index 27d9988..4f2f45e 100644 --- a/include/StackUsageAnalyzer.hpp +++ b/include/StackUsageAnalyzer.hpp @@ -22,6 +22,7 @@ namespace llvm namespace ctrace::stack::analysis { + struct AnalyzedUnits; class CompilationDatabase; struct ConstPointeeParamIndex; struct GlobalReadBeforeWriteSummaryIndex; @@ -81,6 +82,9 @@ namespace ctrace::stack // Max stacks and recursion over one call graph of the modules analyzed together, which // they were prepared for: the pipeline then leaves their IR as it is. std::shared_ptr globalStackFacts; + // The inputs analyzed together, for the Clang check of const suggestions (#217). Null: + // the suggestions say they were not checked. + std::shared_ptr analyzedUnits; std::vector excludeDirs; std::vector extraCompileArgs; @@ -126,7 +130,9 @@ namespace ctrace::stack std::uint32_t resourceSummaryMemoryOnly : 1 = 0; std::uint32_t warningsOnly : 1 = 0; std::uint32_t assumeExternalFrame : 1 = 0; - std::uint32_t reservedFlags : 17 = 0; + // Experimental: check each const suggestion with Clang before reporting it. + std::uint32_t constSuggestionCheck : 1 = 0; + std::uint32_t reservedFlags : 16 = 0; }; // Per-function result diff --git a/include/analysis/ConstParamAnalysis.hpp b/include/analysis/ConstParamAnalysis.hpp index 3790d48..e4b0eae 100644 --- a/include/analysis/ConstParamAnalysis.hpp +++ b/include/analysis/ConstParamAnalysis.hpp @@ -24,6 +24,8 @@ namespace ctrace::stack::analysis std::string currentType; std::string suggestedType; std::string suggestedTypeAlt; + // Lines that say how the suggestion was checked with Clang, or why it could not be (#217). + std::vector checkLines; double confidence = -1.0; std::uint64_t pointerConstOnly : 1 = false; // ex: T * const param std::uint64_t isReference : 1 = false; diff --git a/include/analysis/ConstSuggestionCheck.hpp b/include/analysis/ConstSuggestionCheck.hpp new file mode 100644 index 0000000..cc39e66 --- /dev/null +++ b/include/analysis/ConstSuggestionCheck.hpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "StackUsageAnalyzer.hpp" +#include "analysis/ConstParamAnalysis.hpp" + +namespace llvm +{ + class Module; +} // namespace llvm + +namespace ctrace::stack::analysis +{ + // One input of the analysis, for the Clang check of const suggestions (#217). + struct AnalyzedUnit + { + std::string filename; // as given to the analyzer + const llvm::Module* module = nullptr; + LanguageType language = LanguageType::Unknown; + std::uint32_t reservedLanguagePadding = 0; + }; + + // What the preprocessing of one unit shows: the files it reads, its identifiers (as 64-bit + // hashes: a collision only adds a recheck), and whether it imports a module, which it does + // not read. + struct UnitScan + { + std::set files; + std::unordered_set identifiers; + bool importsModule = false; + std::uint8_t reservedPadding[7] = {}; + }; + + // How the check compiles one unit. + struct UnitCommand + { + std::vector args; // the driver's, as compilerlib builds them + std::string workingDir; + }; + + // The inputs analyzed together. + struct AnalyzedUnits + { + std::vector units; + // By path, found when first needed: the compile command of each unit (none when it has + // none), and its scan. + mutable std::mutex scansMutex; + mutable std::map> commands; + mutable std::map scans; + }; + + // The one input of an analysis that loads it alone. + std::shared_ptr analyzedUnitOf(const std::string& filename, + const llvm::Module& module); + + // A symbol of a unit's compilation before a rewrite, and its counterpart after it, as the + // compilation facts establish them (#217, §5.7). + struct SymbolPair + { + std::string before; + std::string after; + }; + + // What __PRETTY_FUNCTION__ prints for the rewritten function, before and after the rewrite. + struct PrintedSignature + { + std::string before; + std::string after; + }; + + // Compares the code generated for one unit before and after a rewrite. Returns the first + // difference, or nullopt when the code matches once normalized: + // - debug information is left out; + // - metadata are compared by the order in which each function or global uses them, not by + // their numbers or contents; + // - attribute groups are compared by contents, not by numbers; + // - the symbols the rewrite renames are paired by `established`, the pairs that the rewrite's + // correspondences establish, and by them only: a symbol that only one side has, and that no + // pair covers, is a difference. + // Two differences that a run can observe are accepted, each only when verified, and described + // in `accepted`: the name of a type renamed with the function (_ZTS holds the mangled name + // of T), and a string constant whose only change is the function's printed signature, from + // `signature` (null: none is accepted). + std::optional rewrittenCodeDifference( + llvm::Module& before, llvm::Module& after, const std::vector& established, + const PrintedSignature* signature = nullptr, std::vector* accepted = nullptr); + + // Checks the suggestions made for `mod` with Clang (#217): rewrites every declaration of the + // parameter with the suggested type, recompiles every unit that sees the change, and compares + // their generated code. The units are the analyzed ones and, with a compilation database, its + // C and C++ files; a unit sees the change when it reads a rewritten file, when its analyzed + // code references the function, or, for a function with external linkage outside any class, + // when it names the function after preprocessing (an operator: when it writes `operator`). A + // suggestion is removed when the rewrite adds an error or a tracked warning, or changes a call + // or reference target, an override or a constant condition, unless the check stops first at a + // fatal error or a change beyond the declarations it plans; it is kept as not checked when it + // cannot be shown valid, a generated code difference included; the others are kept as checked. + // Each gets detail lines that say what was checked, or why it could not be. + void checkConstSuggestions(const llvm::Module& mod, std::vector& issues, + const AnalysisConfig& config); +} // namespace ctrace::stack::analysis diff --git a/include/analysis/InputPipeline.hpp b/include/analysis/InputPipeline.hpp index f641e6a..427354a 100644 --- a/include/analysis/InputPipeline.hpp +++ b/include/analysis/InputPipeline.hpp @@ -2,6 +2,7 @@ #pragma once #include +#include #include #include #include @@ -33,4 +34,15 @@ namespace ctrace::stack::analysis ModuleLoadResult loadModuleForAnalysis(const std::string& filename, const AnalysisConfig& config, llvm::LLVMContext& ctx, llvm::SMDiagnostic& err); + + // The compile command the analysis uses for a C or C++ input: the arguments it gives the + // compiler, input included, and the directory to compile in (empty: the current one). + bool buildSourceCompileCommand(const std::string& filename, LanguageType language, + const AnalysisConfig& config, std::vector& args, + std::string& workingDir, std::string& error); + + // Runs `compile` while no other compilation runs, in `workingDir` when it is not empty: + // Clang and LLVM keep global state, and a compile command's directory is the process's. + bool runSerializedCompile(const std::string& workingDir, const std::function& compile, + std::string& error); } // namespace ctrace::stack::analysis diff --git a/include/mangle.hpp b/include/mangle.hpp index bb17b2e..de229d1 100644 --- a/include/mangle.hpp +++ b/include/mangle.hpp @@ -8,13 +8,6 @@ namespace ctrace_tools { - /** - * @brief Concept to define types that can be converted to `std::string_view`. - * - * The `StringLike` concept ensures that any type passed to functions - * requiring it can be implicitly converted to `std::string_view`. - */ - template concept StringLike = std::convertible_to; // TODO: add mangling for windows /** @@ -23,32 +16,19 @@ namespace ctrace_tools * This function determines whether a given name follows the Itanium C++ ABI * mangling conventions (e.g., names starting with `_Z`). * - * @tparam T A type satisfying the `StringLike` concept. * @param name The name to check for mangling. * @return `true` if the name is mangled, `false` otherwise. * - * @note This function uses `abi::__cxa_demangle` to attempt demangling. - * If the demangling succeeds, the name is considered mangled. + * @note The name is mangled if a demangler reads it: the C++ runtime's + * (`abi::__cxa_demangle`), or LLVM's when the runtime's predates the + * name's mangling, as a C++23 explicit object member's on macOS 14. * @note This implementation is specific to platforms using the Itanium C++ ABI * (e.g., Linux, macOS). Windows mangling is not yet supported. * @note The function is marked `[[nodiscard]]`, meaning the return value * should not be ignored. It is also `noexcept`, indicating that it * does not throw exceptions. */ - [[nodiscard]] bool isMangled(StringLike auto name) noexcept - { - int status = 0; - std::string_view sv{name}; - - if (sv.length() < 2 || sv.substr(0, 2) != "_Z") - { - return false; - } - - std::unique_ptr demangled( - abi::__cxa_demangle(sv.data(), nullptr, nullptr, &status), std::free); - return status == 0; - } + [[nodiscard]] bool isMangled(std::string_view name) noexcept; /** * @brief Generates a mangled name for a function. diff --git a/main.cpp b/main.cpp index 5ad7ec0..fc496dc 100644 --- a/main.cpp +++ b/main.cpp @@ -79,6 +79,14 @@ static void printHelp() << " --compile-ir-format=bc|ll Compilation IR format for source inputs (default: bc)\n" << " --resource-summary-cache-memory-only Use in-memory cache only for cross-TU " "summaries\n" + << " --experimental-const-suggestion-check Experimental: check each const " + "suggestion\n" + << " with Clang: removed if the rewrite adds an error or a\n" + << " tracked warning, or changes a call or reference target,\n" + << " an override or a constant condition, unless the check\n" + << " stops first at a fatal error or a change beyond the\n" + << " declarations it plans; kept as not checked when it\n" + << " cannot be shown valid (off by default)\n" << " --uninitialized-cross-tu Enable cross-TU uninitialized summaries (default: on)\n" << " --no-uninitialized-cross-tu Disable cross-TU uninitialized summaries\n" << " --only-file= Only report functions from this source file\n" @@ -170,6 +178,8 @@ static void printEffectiveConfig(const ctrace::stack::cli::ParsedArguments& pars llvm::errs() << "warnings-only: " << (cfg.warningsOnly ? "true" : "false") << "\n"; llvm::errs() << "quiet: " << (cfg.quiet ? "true" : "false") << "\n"; llvm::errs() << "demangle: " << (cfg.demangle ? "true" : "false") << "\n"; + llvm::errs() << "experimental-const-suggestion-check: " + << (cfg.constSuggestionCheck ? "true" : "false") << "\n"; llvm::errs() << "resource-model: " << (cfg.resourceModelPath.empty() ? "" : cfg.resourceModelPath) << "\n"; llvm::errs() << "escape-model: " diff --git a/run_test.py b/run_test.py index 1d44a59..b153a9d 100755 --- a/run_test.py +++ b/run_test.py @@ -99,6 +99,8 @@ def _get_file_hash(p: Path) -> str: _RE_RESOURCE_MODEL = re.compile(r"//\s*resource-model\s*[:=]\s*(\S+)", re.IGNORECASE) _RE_ESCAPE_MODEL = re.compile(r"//\s*escape-model\s*[:=]\s*(\S+)", re.IGNORECASE) _RE_BUFFER_MODEL = re.compile(r"//\s*buffer-model\s*[:=]\s*(\S+)", re.IGNORECASE) +# `// analyzer-arg: `: one more analyzer argument for both passes of the fixture. +_RE_ANALYZER_ARG = re.compile(r"//\s*analyzer-arg\s*[:=]\s*(\S+)", re.IGNORECASE) _RE_STRICT_DIAG = re.compile(r"//\s*strict-diagnostic-count\s*[:=]\s*(\S+)", re.IGNORECASE) _RE_STRICT_DETAILS = re.compile(r"//\s*strict-expectation-details\s*[:=]\s*(\S+)", re.IGNORECASE) # `// [smt-z3] not contains: ...` or `// [default] at line ...`: an expectation for one pass. @@ -361,6 +363,7 @@ def extract_expectations(c_path: Path): resource_model = None escape_model = None buffer_model = None + analyzer_args = [] strict_diag_count = None strict_details = None lines = c_path.read_text().splitlines() @@ -399,6 +402,11 @@ def parse_bool_directive(value: str): buffer_model = buffer_match.group(1) i += 1 continue + analyzer_arg_match = _RE_ANALYZER_ARG.match(stripped) + if analyzer_arg_match: + analyzer_args.append(analyzer_arg_match.group(1)) + i += 1 + continue strict_match = _RE_STRICT_DIAG.match(stripped) if strict_match: parsed = parse_bool_directive(strict_match.group(1)) @@ -467,6 +475,7 @@ def parse_bool_directive(value: str): strict_diag_count, strict_details, unknown_scopes, + analyzer_args, ) @@ -1008,15 +1017,17 @@ def parse_human_diagnostic_messages(output: str): def _check_human_vs_json_parity_sample(sample: Path): lines = [] sample_ok = True + # The arguments the fixture declares with `// analyzer-arg:`, as check_file passes them. + *_, fixture_args = extract_expectations(sample) - human = run_analyzer([str(sample)]) + human = run_analyzer([str(sample), *fixture_args]) if human.returncode != 0: lines.append(f" ❌ human run failed for {sample} (code {human.returncode})") lines.append(human.stdout or "") lines.append(human.stderr or "") return False, "\n".join(lines).rstrip() + "\n" - structured = run_analyzer([str(sample), "--format=json"]) + structured = run_analyzer([str(sample), "--format=json", *fixture_args]) if structured.returncode != 0: lines.append(f" ❌ json run failed for {sample} (code {structured.returncode})") lines.append(structured.stdout or "") @@ -2765,11 +2776,12 @@ def _check_cross_tu_rule( rule_id: str, cases: list[tuple[str, list[str], list[str]]], copied_cases: list[tuple[str, list[str], list[str]]], + extra_args: tuple[str, ...] = (), ) -> bool: """ Runs each case in both passes: the analyzer over its fixtures, then the functions reported under rule_id. The analyzer sorts its inputs: copied_cases run on copies named so that the - files keep the listed order. + files keep the listed order. extra_args go to every run. """ print(f"=== Testing {title} ===") fixtures = RUN_CONFIG.test_dir / directory @@ -2811,7 +2823,7 @@ def reported(files: list[str], extra: list[str]) -> Optional[list[str]]: runs.append((f"{pass_name}, {label}", files, pass_args, expected)) for label, files, extra, expected in runs: - found = reported(files, extra) + found = reported(files, [*extra, *extra_args]) if found == expected: print(f" ✅ {label}: {found}") else: @@ -2858,6 +2870,1934 @@ def check_const_param_contract_message_across_files() -> bool: print() return True + +def check_const_suggestion_cross_tu() -> bool: + """ + #217: a suggested const type is reported only if every analyzed file still compiles with it, + once every declaration the defining file sees is rewritten. Another analyzed file that stores + the function where a non-const parameter is expected removes the suggestion. One that declares + it itself, without the header, or converts its address with an explicit cast, keeps it, not + checked: whether that matters depends on how the files are linked, or on what the cast hides. + A homonym with internal linkage in another file does not change it. A file that is not + analyzed is not checked. The callback and cast files sort before the definition, the + redeclaration after it. + """ + return _check_cross_tu_rule( + "suggested const types checked in every analyzed file", + "pointer_reference-const_correctness", + "cross-tu-const-suggestion-", + "ConstParameterNotModified.Pointer", + [ + ("def alone", ["def.c"], ["read_value"]), + ("def and a caller", ["def.c", "caller.c"], ["read_value"]), + ("def and a callback of the old type", ["def.c", "callback.c"], []), + ("def and a redeclaration", ["def.c", "redecl.c"], ["read_value"]), + ("def and an explicit cast", ["def.c", "cast.c"], ["read_value"]), + # Another file's homonym with internal linkage is another function. + ( + "a static function and a static homonym", + ["static-read.c", "static-write.c"], + ["helper"], + ), + ( + "an anonymous namespace's function and its homonym", + ["anonymous-read.cpp", "anonymous-write.cpp"], + ["_ZN12_GLOBAL__N_16helperEPi"], + ), + ], + [], + ("--experimental-const-suggestion-check",), + ) + + +def check_const_suggestion_without_source() -> bool: + """ + #217: a suggestion made on LLVM IR, with no source to rewrite, is kept and says that the + suggested type was not checked with Clang. So is a suggestion for a function that an analyzed + IR input uses: that input cannot be recompiled. Neither claims the Clang check. + """ + print("=== Testing const suggestions without source ===") + fixtures = RUN_CONFIG.test_dir / "pointer_reference-const_correctness" + checked = "checked in the analyzed files only" + + def message(files: list[Path], function: str) -> Optional[str]: + result = run_analyzer( + [*map(str, files), "--format=json", "--experimental-const-suggestion-check"] + ) + try: + diagnostics = json.loads(result.stdout or "").get("diagnostics", []) + except json.JSONDecodeError: + return None + for d in diagnostics: + if str(d.get("ruleId", "")).startswith("ConstParameterNotModified") and str( + d.get("location", {}).get("function", "") + ) == function: + return str(d.get("details", {}).get("message", "")) + return None + + ok = True + for label, files, function, expected in ( + ( + "IR input alone", + [fixtures / "const-suggestion-ir-input.ll"], + "read_value_ir", + "not checked with Clang: no source file, so whether the suggested type compiles " + "was not verified", + ), + ( + "source function used by an IR input", + [ + fixtures / "cross-tu-const-suggestion-def.c", + fixtures / "cross-tu-const-suggestion-ir-caller.ll", + ], + "read_value", + "cross-tu-const-suggestion-ir-caller.ll uses the function but has no source", + ), + ): + text = message(files, function) + if text is not None and expected in text and checked not in text: + print(f" ✅ {label}") + else: + print(f" ❌ {label}: {text!r}") + ok = False + print() + return ok + + +def check_const_suggestion_compilation_database() -> bool: + """ + #217: with a compilation database, a suggested const type is checked in every file the + database lists that sees the change, analyzed or not, with its own compile command. A file + that is not analyzed and stores the function where a non-const parameter is expected removes + the suggestion, even listed after a file that cannot be checked. So does a file that takes the + address of a method with the old type: it sees the change through the header alone. A file + that declares the function itself, without the header, keeps the suggestion as not checked, + and the reason names it and the link it assumes; so does a file that converts the function's + address with an explicit cast, and the reason names the cast. A file that sees the change but + does not compile as it is, or cannot be preprocessed, keeps the suggestion as not checked, + without the claim of the check. A file that does not see the change is not compiled. + """ + print("=== Testing const suggestions checked in the compilation database ===") + # Absolute: a compilation database names absolute directories. + fixtures = (RUN_CONFIG.test_dir / "pointer_reference-const_correctness").resolve() + prefix = "cross-tu-const-suggestion-" + checked = "checked in every file of the compilation database that sees the change" + method = "_ZNK6Reader4readEPi" + generated = { + "broken.c": f'#include "{prefix}api.h"\nint broken(void) {{ return read_value(0) }}\n', + "missing.c": '#include "no-such-header.h"\n', + "unrelated.c": "int unrelated(void) { return 0 }\n", + # The database lists its files sorted: this callback comes after broken.c. + "stores-reader.c": f'#include "{prefix}api.h"\n' + "int (*const stored_reader)(int*) = read_value;\n", + "method.hpp": "struct Reader\n{\n int read(int* p) const;\n};\n", + "method.cpp": '#include "method.hpp"\n' + "int Reader::read(int* p) const\n{\n return *p;\n}\n", + "member-pointer.cpp": '#include "method.hpp"\n' + "int (Reader::*const stored)(int*) const = &Reader::read;\n", + } + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_database_") as tmp: + # Resolved: the analyzer names files by their real path. + root = Path(tmp).resolve() + for name, text in generated.items(): + (root / name).write_text(text) + + def message(label: str, listed: list[str], function: str) -> tuple[bool, Optional[str]]: + """Whether the analysis ran, and the message of the suggestion, if it is reported.""" + files = [root / f if f in generated else fixtures / f"{prefix}{f}" for f in listed] + database = root / label.replace(" ", "-").replace("'", "") / "compile_commands.json" + database.parent.mkdir() + database.write_text( + json.dumps( + [ + { + "directory": str(fixtures), + "file": str(f), + "arguments": ( + ["clang++", "-std=c++17"] if f.suffix == ".cpp" else ["clang"] + ) + + ["-I", str(fixtures), "-c", str(f)], + } + for f in files + ] + ) + ) + # The database is an input the cache key does not cover. + result = run_analyzer_uncached( + [ + str(files[0]), + f"--compile-commands={database}", + "--format=json", + "--experimental-const-suggestion-check", + ] + ) + try: + diagnostics = json.loads(result.stdout or "").get("diagnostics", []) + except json.JSONDecodeError: + return False, None + ran = result.returncode == 0 + for d in diagnostics: + if str(d.get("ruleId", "")).startswith("ConstParameterNotModified") and str( + d.get("location", {}).get("function", "") + ) == function: + return ran, str(d.get("details", {}).get("message", "")) + return ran, None + + for label, listed, function, expected in ( + ("a caller", ["def.c", "caller.c"], "read_value", checked), + ( + "a callback of the old type, not analyzed", + ["def.c", "callback.c"], + "read_value", + None, + ), + # Without the header: only the name of the function shows that it sees the change. + ( + "a redeclaration, not analyzed", + ["def.c", "redecl.c"], + "read_value", + "not checked with Clang: " + str(fixtures / f"{prefix}redecl.c") + + " declares read_value with its current type and does not see the rewrite: if " + + str(fixtures / f"{prefix}def.c") + " and " + str(fixtures / f"{prefix}redecl.c") + + " are linked together, that declaration no longer matches the rewritten " + "definition", + ), + ( + "an explicit cast, not analyzed", + ["def.c", "cast.c"], + "read_value", + "not checked with Clang: " + str(fixtures / f"{prefix}cast.c") + + ":10 converts the address of read_value with an explicit cast", + ), + ("a method alone", ["method.cpp"], method, checked), + ( + "a method's address of the old type, not analyzed", + ["method.cpp", "member-pointer.cpp"], + method, + None, + ), + ( + "a file that does not compile as it is", + ["def.c", "broken.c"], + "read_value", + "not checked with Clang: " + str(root / "broken.c") + + " does not compile with Clang as it is", + ), + ( + "a file that cannot be preprocessed", + ["def.c", "missing.c"], + "read_value", + "not checked with Clang: " + str(root / "missing.c") + + " cannot be preprocessed with Clang", + ), + ( + "a file that cannot be checked, then a callback", + ["def.c", "broken.c", "stores-reader.c"], + "read_value", + None, + ), + ( + "a file that does not see the change", + ["def.c", "unrelated.c"], + "read_value", + checked, + ), + ): + ran, text = message(label, listed, function) + if not ran: + good = False + elif expected is None: + good = text is None + else: + good = text is not None and expected in text + if expected != checked: + good = good and checked not in text + detail = f": {text!r}" if ran else ": the analysis did not run" + print(f" {'✅' if good else '❌'} {label}" + ("" if good else detail)) + ok = ok and good + print() + return ok + + +def check_const_suggestion_check_is_opt_in() -> bool: + """ + #217: the Clang check of const suggestions is experimental and off by default. Without + --experimental-const-suggestion-check, the suggestions are reported as the IR analysis finds + them: one whose suggested type does not compile is still reported, and no line says how a + suggestion was checked. With the option, that suggestion is removed, and the one that compiles + says how it was checked. + """ + print("=== Testing the Clang check of const suggestions as an opt-in ===") + fixture = ( + RUN_CONFIG.test_dir + / "pointer_reference-const_correctness" + / "const-suggestion-must-compile.c" + ) + + def messages(extra: list[str]) -> Optional[dict[str, str]]: + result = run_analyzer([str(fixture), "--format=json", *extra]) + if result.returncode != 0: + return None + try: + diagnostics = json.loads(result.stdout or "").get("diagnostics", []) + except json.JSONDecodeError: + return None + return { + str(d.get("location", {}).get("function", "")): str( + d.get("details", {}).get("message", "") + ) + for d in diagnostics + if str(d.get("ruleId", "")).startswith("ConstParameterNotModified") + } + + without = messages([]) + checked = messages(["--experimental-const-suggestion-check"]) + claims = ("checked in the", "not checked with Clang") + ok = True + for label, good in ( + ( + "without the option, a suggested type that does not compile is reported", + without is not None and "writer" in without and "read_slot" in without, + ), + ( + "without the option, no suggestion says how it was checked", + without is not None + and not any(claim in text for text in without.values() for claim in claims), + ), + ( + "with the option, the suggested types that do not compile are removed", + checked is not None and "writer" not in checked and "read_slot" not in checked, + ), + ( + "with the option, the suggestion that compiles says how it was checked", + checked is not None + and "checked in the analyzed files only" in checked.get("read_value", ""), + ), + ): + print(f" {'✅' if good else '❌'} {label}") + ok = ok and good + print() + return ok + + +def check_const_suggestion_line_endings() -> bool: + """ + #217: a type written on a continuation line of a #define is in the macro definition, whether + the file ends its lines with LF or CRLF, or puts spaces between the backslash and the line + break, which Clang accepts with a warning: rewriting it would change every expansion of the + macro, here a function that writes through p. The suggestion is kept as not checked in each + case. + """ + print("=== Testing a macro definition's continuation lines, with LF, CRLF and spaces ===") + source = ( + "#define PARAM(type) type p\n" + "#define DECLARE(name) \\\n" + " int name(PARAM(int*))\n" + "DECLARE(read_first) { return *p; }\n" + "DECLARE(write_second) { *p = 1; return 0; }\n" + ) + expected = "not checked with Clang: the declaration is written by a macro definition" + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_line_endings_") as tmp: + for label, contents in ( + ("LF", source), + ("CRLF", source.replace("\n", "\r\n")), + ("spaces after the backslash", source.replace("\\\n", "\\ \n")), + ): + path = Path(tmp).resolve() / f"reader-{label.lower().replace(' ', '-')}.c" + path.write_bytes(contents.encode("utf-8")) + # The file's bytes are an input the cache key does not cover. + result = run_analyzer_uncached( + [str(path), "--format=json", "--experimental-const-suggestion-check"] + ) + text = None + try: + for d in json.loads(result.stdout or "").get("diagnostics", []): + if str(d.get("ruleId", "")).startswith("ConstParameterNotModified") and str( + d.get("location", {}).get("function", "") + ) == "read_first": + text = str(d.get("details", {}).get("message", "")) + except json.JSONDecodeError: + pass + good = result.returncode == 0 and text is not None and expected in text + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {text!r}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_removal_reason() -> bool: + """ + #217: the trace of a removed suggestion names the first diagnostic that the rewrite adds, not + one the file had before it, and says whether it is an error or a checked warning: in C, a + qualifier lost in a nested pointer type is a warning, and the code still compiles. + """ + print("=== Testing the reason given for a removed const suggestion ===") + trace = re.compile(r"\[const-suggestion\] (\S+) parameter (\S+): (.*)") + + def reasons(path: Path) -> dict[str, str]: + result = run_analyzer_uncached( + [str(path), "--experimental-const-suggestion-check", "--verbose"] + ) + out = {} + for line in (result.stdout + result.stderr).splitlines(): + match = trace.search(line) + if match: + out[f"{match.group(1)}/{match.group(2)}"] = match.group(3) + return out + + fixture = ( + RUN_CONFIG.test_dir + / "pointer_reference-const_correctness" + / "const-suggestion-must-compile.c" + ).resolve() + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_reason_") as tmp: + before = Path(tmp).resolve() / "warning-before.c" + before.write_text( + "static int read_value(int* p) { return *p; }\n" + "int* drop_const(const int* q) { return q; }\n" + "int (*const reader)(int*) = read_value;\n" + ) + fixture_reasons = reasons(fixture) + before_reasons = reasons(before) + for label, text, expected in ( + ( + "a qualifier lost in a nested pointer type is a checked warning", + fixture_reasons.get("address_to_int_pp/p", ""), + f"removed, gains a checked warning: {fixture}:46:", + ), + ( + "a function pointer of the old type is an error", + fixture_reasons.get("read_slot/slot", ""), + f"removed, does not compile: {fixture}:36:", + ), + ( + "the diagnostic the file had before the rewrite is not the reason", + before_reasons.get("read_value/p", ""), + f"removed, does not compile: {before}:3:", + ), + ): + good = text.startswith(expected) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {text!r}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_both_modes() -> bool: + """ + #217: each const-suggestion fixture, and the IR input, analyzed without and with + --experimental-const-suggestion-check. Without the option, no suggestion says how it was + checked. With it, no suggestion appears that the IR analysis did not report, and each one kept + says how it was checked. Across these inputs, the option removes suggestions, keeps checked + ones, and keeps ones it could not check. + """ + print("=== Testing const suggestions without and with the experimental check ===") + directory = RUN_CONFIG.test_dir / "pointer_reference-const_correctness" + inputs = sorted( + p for p in directory.glob("const-suggestion-*") if p.suffix in (".c", ".cpp", ".ll") + ) + checked = "↳ checked in the" + not_checked = "↳ not checked with Clang" + claims = (checked, not_checked, "↳ ABI:", "↳ difference:") + parameter = re.compile(r"parameter '([^']+)' in function") + + def suggestions(path: Path, extra: list[str]) -> Optional[dict[tuple[str, str], str]]: + result = run_analyzer([str(path), "--format=json", *extra]) + if result.returncode != 0: + return None + try: + diagnostics = json.loads(result.stdout or "").get("diagnostics", []) + except json.JSONDecodeError: + return None + out = {} + for d in diagnostics: + if not str(d.get("ruleId", "")).startswith("ConstParameterNotModified"): + continue + message = str(d.get("details", {}).get("message", "")) + match = parameter.search(message) + function = str(d.get("location", {}).get("function", "")) + out[(function, match.group(1) if match else "")] = message + return out + + option = "--experimental-const-suggestion-check" + ok = True + outcomes = {"removed": 0, "kept, checked": 0, "kept, not checked": 0} + for path in inputs: + # The fixture's other arguments (`// analyzer-arg:`), in both runs. + *_, declared = extract_expectations(path) + others = [arg for arg in declared if arg != option] + without = suggestions(path, others) + with_option = suggestions(path, [*others, option]) + problems = [] + if without is None or with_option is None: + problems.append("the analysis did not run") + else: + for key, text in without.items(): + if any(claim in text for claim in claims): + problems.append(f"without the option, {key} says how it was checked") + for key, text in with_option.items(): + if key not in without: + problems.append(f"{key} is reported only with the option") + elif not_checked in text: + outcomes["kept, not checked"] += 1 + elif checked in text: + outcomes["kept, checked"] += 1 + else: + problems.append(f"with the option, {key} does not say how it was checked") + outcomes["removed"] += sum(1 for key in without if key not in with_option) + print(f" {'❌' if problems else '✅'} {path.name}") + for problem in problems: + print(f" {problem}") + ok = ok and not problems + for outcome, count in outcomes.items(): + good = count > 0 + print(f" {'✅' if good else '❌'} the option has suggestions {outcome}: {count}") + ok = ok and good + print() + return ok + + +def _const_suggestion_run( + args: list[str], env_overrides: Optional[dict[str, str]] = None +) -> Optional[tuple[dict[str, list[str]], list[str], str]]: + """ + #217: runs the analyzer with the Clang check of const suggestions and its trace (--verbose), on + inputs the cache key does not cover. Returns the messages of the const suggestions by function + (a function reported twice, such as a constructor's two variants, has two), the trace's lines + without their "[const-suggestion] " prefix, and the whole output; None when the analysis did + not run. + """ + result = run_analyzer_uncached( + [*args, "--format=json", "--experimental-const-suggestion-check", "--verbose"], + env_overrides, + ) + if result.returncode != 0: + return None + try: + diagnostics = json.loads(result.stdout or "").get("diagnostics", []) + except json.JSONDecodeError: + return None + messages: dict[str, list[str]] = {} + for d in diagnostics: + if str(d.get("ruleId", "")).startswith("ConstParameterNotModified"): + function = str(d.get("location", {}).get("function", "")) + messages.setdefault(function, []).append(str(d.get("details", {}).get("message", ""))) + output = (result.stdout or "") + (result.stderr or "") + trace = [ + line.split("[const-suggestion] ", 1)[1] + for line in output.splitlines() + if "[const-suggestion] " in line + ] + return messages, trace, output + + +def check_const_suggestion_one_configuration_per_file() -> bool: + """ + #217: a compilation database can list a file twice, with different commands. The analysis + loads one command per file, and the Clang check of const suggestions compiles each file with + that one only: its trace names one configuration for each file, here the defining file, + analyzed, and a caller that only the database lists. + """ + print("=== Testing const suggestions with files the compilation database lists twice ===") + fixtures = (RUN_CONFIG.test_dir / "pointer_reference-const_correctness").resolve() + definition = fixtures / "cross-tu-const-suggestion-def.c" + caller = fixtures / "cross-tu-const-suggestion-caller.c" + unit = re.compile(r"read_value parameter p, unit (\S+) \[([0-9a-f]+)\]: ") + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_twice_") as tmp: + database = Path(tmp).resolve() / "compile_commands.json" + # A macro the files do not use: the two commands differ, the code does not. + database.write_text( + json.dumps( + [ + { + "directory": str(fixtures), + "file": str(f), + "arguments": ["clang", *extra, "-I", str(fixtures), "-c", str(f)], + } + for f in (definition, caller) + for extra in ([], ["-DLISTED_TWICE"]) + ] + ) + ) + run = _const_suggestion_run([str(definition), f"--compile-commands={database}"]) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + configurations: dict[str, set[str]] = {} + for line in trace: + match = unit.match(line) + if match: + configurations.setdefault(match.group(1), set()).add(match.group(2)) + checked = "checked in every file of the compilation database that sees the change" + ok = True + for label, good in ( + ( + "the suggestion is checked in the compilation database", + any(checked in text for text in messages.get("read_value", [])), + ), + ( + "one configuration of the defining file", + len(configurations.get(str(definition), ())) == 1, + ), + ("one configuration of the caller", len(configurations.get(str(caller), ())) == 1), + ): + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {configurations}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_compiles_each_file_once() -> bool: + """ + #217: the Clang check compiles each file that sees a change once as it is, for all the + suggestions it checks there: here two functions, defined in one file and called in another. + Each file is compiled once before the rewrites (`--timing`), and both suggestions are checked. + """ + print("=== Testing that each file is compiled once as it is for several const suggestions ===") + sources = { + "readers.h": "int read_a(int* p);\nint read_b(int* q);\n", + "readers.c": '#include "readers.h"\n' + "int read_a(int* p)\n{\n return *p;\n}\n" + "int read_b(int* q)\n{\n return *q + 1;\n}\n", + "user.c": '#include "readers.h"\n' + "int use_both(void)\n{\n int v = 1;\n return read_a(&v) + read_b(&v);\n}\n", + } + calls = re.compile(r"const_suggestion\.compile\.(\w+) .*\bcalls=(\d+)") + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_once_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + # Every hotspot, not the 20 longest. + run = _const_suggestion_run( + [str(root / "readers.c"), str(root / "user.c"), "--timing"], + {"CTRACE_HOTSPOT_TOP": "200"}, + ) + if run is None: + return fail_check("the analysis did not run") + messages, _, output = run + compiled = {m.group(1): int(m.group(2)) for m in calls.finditer(output)} + checked = "checked in the analyzed files only" + ok = True + for label, good in ( + ( + "both suggestions are checked", + all(any(checked in t for t in messages.get(f, [])) for f in ("read_a", "read_b")), + ), + ("the defining file is compiled once as it is", compiled.get("defining_before") == 1), + ("the other file is compiled once as it is", compiled.get("other_before") == 1), + ): + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {compiled}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_operator_used_elsewhere() -> bool: + """ + #217: a function is found by its declaration in every file that sees the change, not by its + name: an operator has no identifier. Here a member operator and a method, declared in a header, + defined in one file and called in another: both suggestions are checked in both files. + """ + print("=== Testing const suggestions for a member operator used in another file ===") + sources = { + "vec.hpp": "struct Vec\n{\n int x;\n bool operator==(Vec& o) const;\n" + " int dot(Vec& o) const;\n};\n", + "vec.cpp": '#include "vec.hpp"\n' + "bool Vec::operator==(Vec& o) const\n{\n return x == o.x;\n}\n" + "int Vec::dot(Vec& o) const\n{\n return x * o.x;\n}\n", + "use.cpp": '#include "vec.hpp"\n' + "int use(Vec a, Vec b)\n{\n return (a == b) + a.dot(b);\n}\n", + } + checked = "checked in the analyzed files only" + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_operator_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + run = _const_suggestion_run([str(root / "vec.cpp"), str(root / "use.cpp")]) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + ok = True + for label, function in (("operator==", "_ZNK3VeceqERS_"), ("dot", "_ZNK3Vec3dotERS_")): + texts = messages.get(function, []) + good = ( + len(texts) == 1 + and checked in texts[0] + and f"{function} parameter o: checked in 2 file(s)" in trace + ) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_homonyms_across_files() -> bool: + """ + #217: in C, a function with internal linkage and a function of the same name with external + linkage in another file are two functions: neither file declares the other's. Whichever of the + two gets the suggestion, the other file does not refute it, and it is checked. + """ + print("=== Testing const suggestions for C homonyms with internal and external linkage ===") + sources = { + "static-reads.c": "static int helper(int* p)\n{\n return *p;\n}\n" + "int use_helper(void)\n{\n int v = 1;\n return helper(&v);\n}\n", + "external-writes.c": "int helper(int* p)\n{\n *p = 2;\n return 0;\n}\n", + "external-reads.c": "int reader(int* p)\n{\n return *p;\n}\n", + "static-writes.c": "static int reader(int* p)\n{\n *p = 3;\n return 0;\n}\n" + "int use_reader(void)\n{\n int v = 1;\n return reader(&v);\n}\n", + } + checked = "checked in the analyzed files only" + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_homonyms_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + for label, files, function in ( + ( + "a static reader, an external writer", + ["static-reads.c", "external-writes.c"], + "helper", + ), + ( + "an external reader, a static writer", + ["external-reads.c", "static-writes.c"], + "reader", + ), + ): + run = _const_suggestion_run([str(root / f) for f in files]) + texts = run[0].get(function, []) if run else [] + good = len(texts) == 1 and checked in texts[0] + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_header_read_differently() -> bool: + """ + #217: a header can mean other declarations in another file, through a macro that file defines + otherwise. Here pair.h declares read_value in def.c, and a global pointer in slot.c: rewriting + the type pair.h writes changes that global's type too, although the generated code does not + show it. The rewrite reaches beyond the suggestion: it is kept, not checked. + """ + print("=== Testing a const suggestion whose header means other declarations elsewhere ===") + sources = { + "pair.h": "PAIR(read_value, int*)\n", + "def.c": '#define PAIR(name, type) int name(type p);\n#include "pair.h"\n#undef PAIR\n' + "int read_value(int* p)\n{\n return *p;\n}\n", + "slot.c": '#define PAIR(name, type) type name##_slot;\n#include "pair.h"\n', + } + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_xmacro_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + run = _const_suggestion_run([str(root / "def.c"), str(root / "slot.c")]) + expected = ( + "not checked with Clang: the rewrite also changes the type of read_value_slot " + f"({root / 'pair.h'}:1)" + ) + texts = run[0].get("read_value", []) if run else [] + good = len(texts) == 1 and expected in texts[0] + detail = "" if good else f": {texts}" + print(f" {'✅' if good else '❌'} the global slot.c declares{detail}") + print() + return good + + +def check_const_suggestion_overload_defined_elsewhere() -> bool: + """ + #217: rewritten, f(int*) takes the symbol of the overload f(const int*), which another file + defines. The two definitions collide only if the files are linked together, which the check + does not know: the suggestion is kept, not checked, and the reason states that assumption, + whether the other file is analyzed or only listed in the compilation database. + """ + print("=== Testing a const suggestion that takes the symbol of another file's overload ===") + sources = { + "api.hpp": "int f(int* p);\nint f(const int* p);\n", + "a.cpp": '#include "api.hpp"\nint f(int* p)\n{\n return *p;\n}\n', + "b.cpp": '#include "api.hpp"\nint f(const int* p)\n{\n return *p + 1;\n}\n', + } + claims = ("checked in the analyzed files only", "checked in every file of the compilation") + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_overload_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + database = root / "compile_commands.json" + database.write_text( + json.dumps( + [ + { + "directory": str(root), + "file": str(root / f), + "arguments": ["clang++", "-std=c++17", "-c", str(root / f)], + } + for f in ("a.cpp", "b.cpp") + ] + ) + ) + expected = ( + f"not checked with Clang: _Z1fPKi is also defined in {root / 'b.cpp'}; if " + f"{root / 'a.cpp'} and {root / 'b.cpp'} are linked together, the two definitions " + "collide" + ) + for label, args in ( + ("both files analyzed", [str(root / "a.cpp"), str(root / "b.cpp")]), + ( + "the other file only in the compilation database", + [str(root / "a.cpp"), f"--compile-commands={database}"], + ), + ): + run = _const_suggestion_run(args) + texts = run[0].get("_Z1fPi", []) if run else [] + good = ( + len(texts) == 1 + and expected in texts[0] + and not any(claim in texts[0] for claim in claims) + ) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_inline_in_a_header() -> bool: + """ + #217: a function defined in a header that two analyzed files include is defined in both: a C + `static inline` function, one function in each file, and a C++ `inline` function, one function + that both files define. Neither definition collides with the other: each suggestion is checked. + """ + print("=== Testing const suggestions for functions defined in a shared header ===") + sources = { + "peek.h": "static inline int peek_c(int* p)\n{\n return *p;\n}\n", + "one.c": '#include "peek.h"\n' + "int use_one(void)\n{\n int v = 1;\n return peek_c(&v);\n}\n", + "two.c": '#include "peek.h"\n' + "int use_two(void)\n{\n int v = 2;\n return peek_c(&v);\n}\n", + "peek.hpp": "inline int peek_cpp(int* p)\n{\n return *p;\n}\n", + "three.cpp": '#include "peek.hpp"\n' + "int use_three()\n{\n int v = 3;\n return peek_cpp(&v);\n}\n", + "four.cpp": '#include "peek.hpp"\n' + "int use_four()\n{\n int v = 4;\n return peek_cpp(&v);\n}\n", + } + checked = "checked in the analyzed files only" + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_inline_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + for label, files, function in ( + ("C, static inline", ["one.c", "two.c"], "peek_c"), + ("C++, inline", ["three.cpp", "four.cpp"], "_Z8peek_cppPi"), + ): + run = _const_suggestion_run([str(root / f) for f in files]) + texts = run[0].get(function, []) if run else [] + # One suggestion for the definition in each file. + good = len(texts) == 2 and all(checked in text for text in texts) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_merged_declaration() -> bool: + """ + #217: rewritten, f(int*) takes the symbol of the overload f(const int*) that the header also + declares: the two declarations become one function, which a file linked with this one may + define. The check does not know how the files are linked: the suggestion is kept, not checked, + and the reason names the declaration and that assumption (ruling 3). + """ + print("=== Testing a const suggestion whose rewrite merges another declaration ===") + sources = { + "api.hpp": "int f(int* p);\nint f(const int* p);\n", + "a.cpp": '#include "api.hpp"\nint f(int* p)\n{\n return *p;\n}\n', + } + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_merged_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + run = _const_suggestion_run([str(root / "a.cpp")]) + expected = ( + "not checked with Clang: the rewrite gives f(int*) the symbol of another declaration " + f"({root / 'api.hpp'}:2); if a file linked with {root / 'a.cpp'} defines it, the two " + "definitions collide" + ) + texts = run[0].get("_Z1fPi", []) if run else [] + good = ( + len(texts) == 1 + and expected in texts[0] + and "checked in the analyzed files only" not in texts[0] + ) + print(f" {'✅' if good else '❌'} a.cpp alone" + ("" if good else f": {texts}")) + print() + return good + + +def check_const_suggestion_strong_and_weak_definitions() -> bool: + """ + #217: rewritten, f(int*) takes the symbol of the overload f(const int*) that another file + defines, and one of the two definitions is weak. Linked together, the files keep the strong + definition and ignore the weak one, without an error: the suggestion is kept, not checked, and + the reason names the definition kept and the one ignored, whether the weak one is the other + file's or the suggested function's. The other file reads no rewritten file and does not + declare f(int*): only the symbol that the rewrite gives f(int*) leads to its definition. + """ + print("=== Testing a const suggestion whose symbol a strong and a weak definition share ===") + weak = "__attribute__((weak)) " + ok = True + for label, entity, other, kept, ignored in ( + ("the other file's definition is weak", "", weak, "a.cpp", "b.cpp"), + ("the suggested function's definition is weak", weak, "", "b.cpp", "a.cpp"), + ): + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_weak_") as tmp: + root = Path(tmp).resolve() + (root / "a.cpp").write_text(entity + "int f(int* p)\n{\n return *p;\n}\n") + (root / "b.cpp").write_text(other + "int f(const int* p)\n{\n return *p + 1;\n}\n") + run = _const_suggestion_run([str(root / "a.cpp"), str(root / "b.cpp")]) + expected = ( + f"not checked with Clang: _Z1fPKi is also defined in {root / 'b.cpp'}; if " + f"{root / 'a.cpp'} and {root / 'b.cpp'} are linked together, the strong " + f"definition, in {root / kept}, is used and the weak one, in {root / ignored}, " + "is ignored" + ) + texts = run[0].get("_Z1fPi", []) if run else [] + good = ( + len(texts) == 1 + and expected in texts[0] + and "checked in the analyzed files only" not in texts[0] + ) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_cast_in_the_defining_file() -> bool: + """ + #217: the defining file converts the function's address with an explicit cast, which alone + leaves the suggestion unchecked, and another analyzed file stores the function where a + non-const parameter is expected, which the rewrite breaks. That file is still checked, and its + error removes the suggestion: a cast never hides an error, a checked warning or a changed fact. + """ + print("=== Testing a const suggestion cast in its own file and broken in another ===") + fixtures = (RUN_CONFIG.test_dir / "pointer_reference-const_correctness").resolve() + header = fixtures / "cross-tu-const-suggestion-api.h" + callback = fixtures / "cross-tu-const-suggestion-callback.c" + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_cast_") as tmp: + definition = Path(tmp).resolve() / "def-with-cast.c" + definition.write_text( + f'#include "{header}"\n\ntypedef int (*erased_reader)(void*);\n\n' + "int read_value(int* p)\n{\n return *p;\n}\n\n" + "erased_reader erase_reader(void)\n{\n return (erased_reader)read_value;\n}\n" + ) + run = _const_suggestion_run([str(definition), str(callback)]) + removed = f"read_value parameter p: removed, does not compile: {callback}:" + good = ( + run is not None + and "read_value" not in run[0] + and any(line.startswith(removed) for line in run[1]) + ) + detail = run[1] if run else "the analysis did not run" + print(f" {'✅' if good else '❌'} removed by the callback" + ("" if good else f": {detail}")) + print() + return good + + +def check_const_suggestion_trace() -> bool: + """ + #217: with --verbose, the trace gives each verdict's grounds after the line of its outcome: the + suggestion (rule, function, the parameter's index among the source's parameters), the + transformation (current type, candidate, alternative) and where it is written, each unit + checked with its configuration and its verdict, then the verdict and its reason. Here one + suggestion verified, one refuted and one not checked. + """ + print("=== Testing the trace of const suggestion verdicts ===") + source = ( + "int read_value(int* p)\n{\n return *p;\n}\n" + "static int read_slot(int* slot)\n{\n return *slot;\n}\n" + "int (*const stored)(int*) = read_slot;\n" + "#define DECLARE(name) int name(int* q)\n" + "DECLARE(read_macro)\n{\n return *q;\n}\n" + ) + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_trace_") as tmp: + path = Path(tmp).resolve() / "verdicts.c" + path.write_text(source) + run = _const_suggestion_run([str(path)]) + if run is None: + return fail_check("the analysis did not run") + _, trace, _ = run + file = re.escape(str(path)) + unit = rf"unit {file} \[[0-9a-f]+\]" + rule = r"ConstParameterNotModified\.Pointer" + expected = { + "verified": [ + r"read_value parameter p: checked in 1 file\(s\)", + rf"read_value parameter p, suggestion: {rule} on read_value, source index 0", + r"read_value parameter p, transformation: int \*p becomes const int \*p " + r"\(pointer to const\)", + rf"read_value parameter p, rewritten at: {file}:1", + rf"read_value parameter p, {unit}: verified", + r"read_value parameter p, verdict: verified", + ], + "refuted": [ + rf"read_slot parameter slot: removed, does not compile: {file}:9:", + rf"read_slot parameter slot, suggestion: {rule} on read_slot, source index 0", + r"read_slot parameter slot, transformation: int \*slot becomes const int \*slot " + r"\(pointer to const\)", + rf"read_slot parameter slot, rewritten at: {file}:5", + rf"read_slot parameter slot, {unit}: refuted: does not compile: {file}:9:", + rf"read_slot parameter slot, verdict: refuted: does not compile: {file}:9:", + ], + "not checked": [ + r"read_macro parameter q: not checked, the declaration is written by a macro " + r"definition", + rf"read_macro parameter q, suggestion: {rule} on read_macro, source index 0", + r"read_macro parameter q, transformation: int \*q becomes const int \*q " + r"\(pointer to const\)", + r"read_macro parameter q, verdict: indeterminate: the declaration is written by a " + r"macro definition", + ], + } + ok = True + for label, patterns in expected.items(): + missing = [p for p in patterns if not any(re.match(p, line) for line in trace)] + good = not missing + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": missing {missing}")) + ok = ok and good + if not ok: + print(" trace:\n" + "\n".join(f" {line}" for line in trace)) + print() + return ok + + +def check_const_suggestion_werror_on_a_tracked_group() -> bool: + """ + #217 (Review Focus 5): a compile command that turns one tracked warning group into errors, + not every warning (-Werror=incompatible-pointer-types-discards-qualifiers), does not change + what the check sees: the group stays a warning in both compilations, and a rewrite that adds + such a warning is removed for that warning, not for an error. + """ + print("=== Testing a tracked warning group that the compile command turns into errors ===") + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_werror_") as tmp: + path = Path(tmp).resolve() / "keep.c" + path.write_text("void keep(int* p)\n{\n int* q = p;\n (void)q;\n}\n") + run = _const_suggestion_run( + [str(path), "--compile-arg=-Werror=incompatible-pointer-types-discards-qualifiers"] + ) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + removed = ( + f"keep parameter p: removed, gains a checked warning: {path}:3: initializing 'int *' " + "with an expression of type 'const int *' discards qualifiers" + ) + good = "keep" not in messages and any(line.startswith(removed) for line in trace) + print(f" {'✅' if good else '❌'} removed for the warning" + ("" if good else f": {trace}")) + print() + return good + + +def check_const_suggestion_conditional_between_callbacks() -> bool: + """ + #239: in C, a conditional between pointers to incompatible types violates a constraint + (C11 6.5.15p3), which Clang reports only as the warning -Wpointer-type-mismatch. use passes f1 + or f2 through `?:`: a rewrite of one of them makes their types differ, so each suggestion on f1 + and f2 is removed for that warning. The witness: pick chooses between a pointer to const and a + pointer of the same type, which C allows: both suggestions on pick are checked. Both hold with + the project's -Werror and the group disabled (D6): the check keeps the group a warning in both + compilations, so the file, whose mix already mixes two incompatible pointers, compiles as it + is, and each removal names the warning, not an error. + """ + print("=== Testing a rewrite that makes the two callbacks of a conditional differ ===") + good = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_conditional_") as tmp: + path = Path(tmp).resolve() / "callbacks.c" + path.write_text( + "typedef struct blk { int x; } blk;\n" + "int api(int (*cb)(blk *, void *), void *data);\n" + "static int f1(blk *b, void *data) { (void)data; return b->x; }\n" + "static int f2(blk *b, void *data) { (void)b; (void)data; return 0; }\n" + "int use(int c) { return api(c ? f1 : f2, 0); }\n" + "int pick(int c, blk *b, blk *other) { const blk *q = c ? b : other; return q->x; }\n" + "int gi;\nlong gl;\n" + "void *mix(int c) { return c ? &gi : ≷ }\n" + ) + for name, extra in ( + ("default warnings", []), + ("-Werror, the group disabled", + ["--compile-arg=-Werror", "--compile-arg=-Wno-pointer-type-mismatch"]), + ): + run = _const_suggestion_run([str(path), *extra]) + messages, trace = (run[0], run[1]) if run else ({}, []) + removed = all( + any(line.startswith(f"{function} parameter {parameter}: removed, gains a checked " + f"warning: {path}:5: pointer type mismatch") for line in trace) + for function in ("f1", "f2") for parameter in ("b", "data") + ) + picked = messages.get("pick", []) + ok = ( + removed and "f1" not in messages and "f2" not in messages + and len(picked) == 2 + and all("checked in the analyzed files only" in t for t in picked) + ) + print(f" {'✅' if ok else '❌'} {name}" + f"{'' if ok else f': {messages}, trace: {trace}'}") + good = good and ok + print() + return good + + +def check_const_suggestion_extern_c_user() -> bool: + """ + #217 (D8): a C function that a C++ file declares through a shared header (extern "C"). Each + file writes the parameter's type in its own language, struct foo * in C and foo * in C++, and + each rewritten declaration is compared with the candidate as its own file writes it: the + suggestion is checked with the C++ user, as with a C user. + """ + print('=== Testing a C function that a C++ file declares through an extern "C" header ===') + sources = { + "api.h": '#ifdef __cplusplus\nextern "C" {\n#endif\nstruct foo { int x; };\n' + "int rd(struct foo* f);\n#ifdef __cplusplus\n}\n#endif\n", + "def.c": '#include "api.h"\nint rd(struct foo* f) { return f->x; }\n', + "user.cpp": '#include "api.h"\nint user(foo* f) { return rd(f); }\n', + "user2.c": '#include "api.h"\nint user2(struct foo* f) { return rd(f); }\n', + } + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_extern_c_") as tmp: + root = Path(tmp).resolve() + for name, text in sources.items(): + (root / name).write_text(text) + for label, user in (("a C++ user", "user.cpp"), ("a C user", "user2.c")): + run = _const_suggestion_run([str(root / "def.c"), str(root / user)]) + messages, trace, _ = run if run else ({}, [], "") + verdicts = [line for line in trace if line.startswith("rd parameter f, verdict: ")] + good = "rd" in messages and verdicts == ["rd parameter f, verdict: verified"] + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {verdicts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_top_level_qualifier() -> bool: + """ + #217 (D18): a parameter's top-level qualifier is not part of the function's type, so a + declaration and the definition can differ in it. The rewrite gives each declaration the + candidate of its own type, and each is compared with that candidate: in both directions, the + suggestion is checked. + """ + print("=== Testing declarations that differ from the definition in a top-level qualifier ===") + source = ( + "int f(int* p);\nint f(int* const p)\n{\n return *p;\n}\n" + "int g(int* const p);\nint g(int* p)\n{\n return *p;\n}\n" + ) + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_top_level_") as tmp: + path = Path(tmp).resolve() / "decls.c" + path.write_text(source) + run = _const_suggestion_run([str(path)]) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + ok = True + for label, function in ( + ("declared without the top-level const, defined with it", "f"), + ("declared with the top-level const, defined without it", "g"), + ): + verdicts = [line for line in trace if line.startswith(f"{function} parameter p, verdict: ")] + good = function in messages and verdicts == [f"{function} parameter p, verdict: verified"] + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {verdicts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_deduced_type_error() -> bool: + """ + #217: a rewrite that makes a call ill-formed changes the type that a declaration deduces from + that call, here a structured binding. The deduced part of a type is not what the rewrite + writes: the declaration stays within the plan (step 3), and the error refutes the suggestion + (step 4). + """ + print("=== Testing a declaration that deduces its type from a call the rewrite breaks ===") + source = ( + "#include \n" + "\n" + "// publish writes the map that publish_all receives: const does not compile.\n" + "void publish_all(std::map& ranges, int key)\n" + "{\n" + " const auto publish = [&ranges](int k)\n" + " {\n" + " const auto [it, inserted] = ranges.try_emplace(k, 0);\n" + " if (!inserted)\n" + " ++it->second;\n" + " };\n" + " publish(key);\n" + "}\n" + ) + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_deduced_") as tmp: + path = Path(tmp).resolve() / "publish.cpp" + path.write_text(source) + run = _const_suggestion_run([str(path), "--compile-arg=-std=c++17"]) + if run is None: + return fail_check("the analysis did not run") + messages, trace, _ = run + removed = ( + f"ranges: removed, does not compile: {path}:8: no matching member function for call to " + "'try_emplace'" + ) + good = not any("publish_all" in name for name in messages) and any( + " parameter " in line and line.split(" parameter ", 1)[1].startswith(removed) + for line in trace + ) + detail = "" if good else f": {[line for line in trace if ' parameter ranges' in line]}" + print(f" {'✅' if good else '❌'} removed for the error{detail}") + print() + return good + + +def check_const_suggestion_written_part_outside_the_plan() -> bool: + """ + #217: a macro argument writes the parameter's type and, outside the plan, a part of another + type that the source writes explicitly: a template argument of a type whose other part is + deduced, or a cast in the initializer of a variable whose type is deduced. The rewrite changes + what is written outside the plan: the suggestion is kept, not checked (step 3), whatever the + deduced part becomes. + """ + print("=== Testing an explicitly written part changed outside the plan ===") + head = "#define PAIR(name, type) int name(type p); " + tail = "PAIR(read_value, int*)\n\nint read_value(int* p)\n{\n return *p;\n}\n" + cases = { + "a written template argument beside a deduced one": ( + "template struct Two\n{\n A a;\n B b;\n};\n" + + head + + "Two name##_two;\n" + ), + "a cast in a deduced declaration's initializer": ( + head + "auto name##_slot = static_cast(nullptr);\n" + ), + } + ok = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_written_part_") as tmp: + for k, (label, prefix) in enumerate(cases.items()): + path = Path(tmp).resolve() / f"pair{k}.cpp" + path.write_text(prefix + tail) + run = _const_suggestion_run([str(path)]) + texts = run[0].get("_Z10read_valuePi", []) if run else [] + good = len(texts) == 1 and ( + "not checked with Clang: the rewrite also changes the type" in texts[0] + ) + print(f" {'✅' if good else '❌'} {label}" + ("" if good else f": {texts}")) + ok = ok and good + print() + return ok + + +def check_const_suggestion_default_argument_outside_the_plan() -> bool: + """ + #217 (D19): only the types that make up a planned parameter's type are the plan's. A macro + argument writes the parameter's type and the type of a cast in its default argument: the + rewrite changes, outside the plan, what the default argument writes, and the suggestion is + kept, not checked. + """ + print("=== Testing a planned parameter's default argument written by its macro argument ===") + source = ( + "#define PARAM(type) type p = static_cast(nullptr)\n" + "\n" + "int read_value(PARAM(int*))\n" + "{\n" + " return p ? *p : 0;\n" + "}\n" + ) + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_default_argument_") as tmp: + path = Path(tmp).resolve() / "default.cpp" + path.write_text(source) + run = _const_suggestion_run([str(path)]) + expected = ( + f"not checked with Clang: the rewrite also changes the type written at {path}:3" + ) + texts = run[0].get("_Z10read_valuePi", []) if run else [] + good = len(texts) == 1 and expected in texts[0] + detail = "" if good else f": {texts}" + print(f" {'✅' if good else '❌'} the default argument's cast{detail}") + print() + return good + + +# #217 (§5.7, step 5): the correspondences of nested entities, end to end. +P2_LAMBDAS_SOURCE = ( + "template void run_all(int n, F&& f)\n" + "{\n" + " for (int i = 0; i < n; ++i)\n" + " f(i);\n" + "}\n" + "\n" + "template int pass(int n, Each&& each)\n" + "{\n" + " int total = 0;\n" + " each(n, [&](int i) { total += i; });\n" + " return total;\n" + "}\n" + "\n" + "int drive(int n)\n" + "{\n" + " return pass(n, [&](int count, auto&& body) { run_all(count, [&](int i) { body(i); }); });\n" + "}\n" +) + +P2_LOCAL_CLASS_SOURCE = ( + "#include \n" + "\n" + "template int count_paths(int n, Pred&& pred)\n" + "{\n" + " struct State\n" + " {\n" + " int value;\n" + " };\n" + " std::deque work;\n" + " work.push_back({n});\n" + " int found = 0;\n" + " while (!work.empty())\n" + " {\n" + " const State state = work.front();\n" + " work.pop_front();\n" + " if (pred(state.value))\n" + " ++found;\n" + " if (state.value > 0)\n" + " work.push_back({state.value - 1});\n" + " }\n" + " return found;\n" + "}\n" + "\n" + "int use(int n)\n" + "{\n" + " return count_paths(n, [](int v) { return v % 2 == 0; });\n" + "}\n" +) + +P2_TWO_INSTANTIATIONS_SOURCE = P2_LAMBDAS_SOURCE.replace( + " each(n, [&](int i) { total += i; });\n", + " each(n, [&](int i) { total += i; });\n each(n, [&](int i) { total -= 2 * i; });\n", +) + +# Everything nested in the generic lambda's call operator, a specialization that step 3 pairs, so +# that P2, not step 4, pairs it: a local class with non-trivial implicit members, a lambda converted +# to a function pointer by apply_both (operator() and __invoke), a class template's hidden friend, +# and probe of three member classes, two of which differ only by a closure type. No lambda is kept in +# a local variable and Box holds a pointer: the __const globals of lambdas kept in locals, which step 5 +# does not pair, and Box's implicit members, which have their own check, are not what this +# check is about. The unit test +# testConstSuggestionNestedIdentities adds a polymorphic local class to this source, whose vtable P2 +# does not pair. +P2_KEYS_SOURCE = ( + "#include \n" + "\n" + "template struct Box\n" + "{\n" + " T* value;\n" + " friend bool operator==(const Box& a, const Box& b) { return a.value == b.value; }\n" + " struct Inner\n" + " {\n" + " int n = 0;\n" + " };\n" + "};\n" + "\n" + "template int probe(T) { return 1; }\n" + "\n" + "template int apply_both(L call)\n" + "{\n" + " int (*pointer)(int) = call;\n" + " return pointer(1) + call(1);\n" + "}\n" + "\n" + "template int pass(int n, Each&& each)\n" + "{\n" + " int total = 0;\n" + " each(n, [&](int i) { total += i; });\n" + " return total;\n" + "}\n" + "\n" + "int drive(int n)\n" + "{\n" + " return pass(n, [&](int count, auto&& body) {\n" + " struct State\n" + " {\n" + " std::vector v;\n" + " };\n" + " State a{{count}};\n" + " State b = a;\n" + " State c = static_cast(a);\n" + " b = c;\n" + " c = static_cast(b);\n" + " State d;\n" + " Box x{nullptr}, y{nullptr};\n" + " const int found = (x == y) + probe(typename Box::Inner{}) +\n" + " probe(typename Box::Inner{}) +\n" + " probe(typename Box::Inner{}) +\n" + " apply_both([](int v) { return 2 * v; });\n" + " body(found + static_cast(b.v.size() + c.v.size() + d.v.size()));\n" + " });\n" + "}\n" +) + +_P2_VERDICT = re.compile(r"^(\S+) parameter (\S+): (checked in \d+ file\(s\)|removed, .*|not checked, .*)$") + + +def _p2_verdicts(name: str, source: str) -> Optional[dict[str, list[str]]]: + """The trace's verdicts by parameter name, in the order of the trace; None if no run.""" + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_p2_") as tmp: + path = Path(tmp).resolve() / name + path.write_text(source) + run = _const_suggestion_run([str(path), "--compile-arg=-std=c++20"]) + if not run: + return None + verdicts: dict[str, list[str]] = {} + for line in run[1]: + match = _P2_VERDICT.match(line) + if match: + verdicts.setdefault(match.group(2), []).append(match.group(3)) + return verdicts + + +def _p2_expect(label: str, verdicts: Optional[dict[str, list[str]]], expected: dict[str, list[str]]) -> bool: + """Each expected parameter's verdicts, by their beginning.""" + good = verdicts is not None and all( + len(verdicts.get(name, [])) == len(starts) + and all(text.startswith(start) for text, start in zip(verdicts[name], starts)) + for name, starts in expected.items() + ) + print(f" {'✅' if good else '❌'} {label}{'' if good else f': {verdicts}'}") + return good + + +def check_const_suggestion_p2_lambdas_of_another_specialization() -> bool: + """ + #217 (§5.7, step 5): a lambda nested in the generic lambda's call operator, a specialization that the + rewrite of pass renames, is the argument of run_all: P2 pairs it through that specialization, + and every suggestion is checked. + """ + print("=== Testing the lambdas nested in another renamed specialization (P2) ===") + good = _p2_expect( + "each, body and f checked", + _p2_verdicts("lambdas.cpp", P2_LAMBDAS_SOURCE), + {"each": ["checked in"], "body": ["checked in"], "f": ["checked in"]}, + ) + print() + return good + + +def check_const_suggestion_p2_local_class_in_a_library_template() -> bool: + """ + #217 (§5.7, step 5): a local class of the rewritten specialization is the argument of + std::deque; the library's members of std::deque, its member classes and their implicit + members among them, pair through State. A member key without the member's type, or member + classes written as Clang prints them, leave pred not checked; so does a standard library whose + implicit members (libstdc++'s _Deque_impl::~_Deque_impl()) would go unidentified. + """ + print("=== Testing a local class as a library template's argument (P2) ===") + good = _p2_expect( + "pred checked", + _p2_verdicts("local_class.cpp", P2_LOCAL_CLASS_SOURCE), + {"pred": ["checked in"]}, + ) + print() + return good + + +def check_const_suggestion_p2_other_specialization_of_the_rewritten_template() -> bool: + """ + #217 (§5.7, step 5): run_all is instantiated twice. Rewriting f in one specialization + rewrites the template, so the other specialization's function type changes too; P2 keeps every + part of a key but the nested types, so that one stays unpaired and both f stay not checked. A + specialization key without the function type would pair it. + """ + print("=== Testing the other specialization of the rewritten template (P2) ===") + good = _p2_expect( + "each and both body checked, both f not checked", + _p2_verdicts("two_instantiations.cpp", P2_TWO_INSTANTIATIONS_SOURCE), + { + "each": ["checked in"], + "body": ["checked in", "checked in"], + "f": ["not checked, the generated code changes", "not checked, the generated code changes"], + }, + ) + print() + return good + + +def check_const_suggestion_p2_keys_keep_candidates_apart() -> bool: + """ + #217 (§5.7, step 5): the candidates nested in the generic lambda's call operator pair one to + one: the local class's implicit constructors and assignments (by the member's type), the + lambda's operator() and __invoke (by name, static flag and qualifiers, which this check protects + together, not each one), the hidden friend of Box (by its function type), probe of member + classes whose specializations differ only by a closure type (through their specialization). A + key without any of these parts merges two of them, and each stays not checked. + """ + print("=== Testing that the keys keep the nested candidates apart (P2) ===") + good = _p2_expect( + "each and body checked", + _p2_verdicts("keys.cpp", P2_KEYS_SOURCE), + {"each": ["checked in"], "body": ["checked in"]}, + ) + print() + return good + + +# The implicit members of classes whose arguments hold a type nested in the generic lambda's call +# operator: Box's default, copy and move constructors, assignments and destructor, the +# destructors of Holder and Holder. No polymorphic class: its vtable is a +# global that P2 does not pair. +P2_IMPLICIT_SOURCE = ( + "#include \n" + "\n" + "template struct Box\n" + "{\n" + " T value;\n" + "};\n" + "\n" + "template struct Holder\n" + "{\n" + " T value;\n" + " U other;\n" + "};\n" + "\n" + "template int pass(int n, Each&& each)\n" + "{\n" + " int total = 0;\n" + " each(n, [&](int i) { total += i; });\n" + " return total;\n" + "}\n" + "\n" + "int drive(int n)\n" + "{\n" + " return pass(n, [&](int count, auto&& body) {\n" + " struct State\n" + " {\n" + " std::vector v;\n" + " };\n" + " Box a{{{count}}};\n" + " Box b = a;\n" + " Box c = static_cast&&>(a);\n" + " b = c;\n" + " c = static_cast&&>(b);\n" + " Box d;\n" + " Holder h{};\n" + " Holder k{};\n" + " body(static_cast(b.value.v.size() + c.value.v.size() + d.value.v.size()) +\n" + " h.other + static_cast(k.other));\n" + " });\n" + "}\n" +) + +# The rewrite of p, only read, changes Tag's own argument, int * becoming const int *: Tag's implicit +# destructor is then another class's, and Tag holds no nested type. (p is not stored in Tag: a stored +# pointer escapes, and no const is suggested; the first version of this check had no suggestion.) +P2_OWNER_CHANGED_SOURCE = ( + "#include \n" + "\n" + "template struct Tag\n" + "{\n" + " std::vector items;\n" + "};\n" + "\n" + "int read_value(int* p)\n" + "{\n" + " Tag tag{};\n" + " return *p + static_cast(tag.items.size());\n" + "}\n" +) + + +def check_const_suggestion_p2_implicit_members_of_a_specialization() -> bool: + """ + #217 (§5.7, step 5): the implicit members of Box and of the two Holders pair through + State, by their class, kind and name, variant and whole type. Without the type, the variant or + the class's other arguments, two of them merge, and each stays not checked. + """ + print("=== Testing the implicit members of specializations (P2) ===") + good = _p2_expect( + "each and body checked", + _p2_verdicts("implicit.cpp", P2_IMPLICIT_SOURCE), + {"each": ["checked in"], "body": ["checked in"]}, + ) + print() + return good + + +def check_const_suggestion_p2_owner_changed_by_the_rewrite() -> bool: + """ + #217 (§5.7, step 5, negative): the rewrite of p changes Tag's own argument, so Tag's + implicit destructor has no counterpart; step 5, which keeps every argument and identifies only + the classes that hold a nested type, leaves it unpaired, and the suggestion stays not checked. A + key that wrote the class by its name alone, whatever it holds, would pair it. + """ + print("=== Testing an implicit member whose class the rewrite changes (P2) ===") + good = _p2_expect( + "p not checked", + _p2_verdicts("owner_changed.cpp", P2_OWNER_CHANGED_SOURCE), + {"p": ["not checked, the generated code changes"]}, + ) + print() + return good + + +def check_const_suggestion_external_dependency_only_declared() -> bool: + """ + #217 (final review of #239, C1): f's rewrite changes the specialization of h it calls, from + h to h. h is only declared in f.cpp; another file defines h by an + explicit instantiation and nothing defines h. Comparing the two declarations proves + nothing about that code, so the suggestion is kept, not checked, naming h. The witness: with h + defined in its header, f.cpp defines the specialization it calls, and the suggestion stays + checked. + """ + print("=== Testing a renamed dependency that the file only declares ===") + good = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_external_dependency_") as tmp: + root = Path(tmp).resolve() + for name, header, extra in ( + ("only declared", "template int h(T p);\n", + "template int h(T p) { return p ? 1 : 0; }\ntemplate int h(int*);\n"), + ("defined in its header", "template int h(T p) { return p ? 1 : 0; }\n", ""), + ): + case = root / name.replace(" ", "_") + case.mkdir() + (case / "h.hpp").write_text(header) + (case / "hdef.cpp").write_text('#include "h.hpp"\n' + extra + "int g() { return 0; }\n") + (case / "f.cpp").write_text( + '#include "h.hpp"\nint f(int* p)\n{\n return *p + h(decltype(p){});\n}\n' + ) + run = _const_suggestion_run([str(case / "f.cpp"), str(case / "hdef.cpp")]) + texts = run[0].get("_Z1fPi", []) if run else [] + if name == "only declared": + ok = ( + len(texts) == 1 + and "not checked with Clang:" in texts[0] + and "int h(int*)" in texts[0] + and "only declares" in texts[0] + ) + else: + ok = len(texts) == 1 and "checked in the analyzed files only" in texts[0] + print(f" {'✅' if ok else '❌'} {name}{'' if ok else f': {texts}'}") + good = good and ok + print() + return good + + +def check_const_suggestion_operator_declared_elsewhere() -> bool: + """ + #217 (final review of #239, C2, D17): a free operator defined in a.cpp is declared again with + its current type in b.cpp, which only the compilation database lists. An operator's name is not + an identifier, so the units that write `operator` are checked for it: b.cpp is judged and the + suggestion is kept, not checked, for that declaration. The witness: b.cpp writes another + operator only; it is judged too, and does not keep the suggestion of a.cpp's own unit from + being checked. In both, the trace names the units judged for each parameter, a.cpp and b.cpp, + with their verdicts. + """ + print("=== Testing a free operator declared again in a file of the database ===") + unit = re.compile(r"_ZeqR1AS0_ parameter ([xy]), unit (\S+) \[[0-9a-f]+\]: (\w+)") + good = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_operator_elsewhere_") as tmp: + root = Path(tmp).resolve() + for name, other, judged in ( + ("declared again", "struct A { int v; };\nbool operator==(A& x, A& y);\n" + "bool same(A& x, A& y) { return x == y; }\n", + {"a.cpp": "verified", "b.cpp": "indeterminate"}), + ("another operator only", "struct B { int v; };\n" + "bool operator<(B x, B y) { return x.v < y.v; }\n", + {"a.cpp": "verified", "b.cpp": "verified"}), + ): + case = root / name.replace(" ", "_") + case.mkdir() + (case / "a.cpp").write_text( + "struct A { int v; };\nbool operator==(A& x, A& y) { return x.v == y.v; }\n" + ) + (case / "b.cpp").write_text(other) + (case / "compile_commands.json").write_text(json.dumps([ + {"directory": str(case), "command": f"clang++ -c {f} -o {f}.o", "file": f} + for f in ("a.cpp", "b.cpp") + ])) + run = _const_suggestion_run([ + str(case / "a.cpp"), f"--compile-commands={case / 'compile_commands.json'}" + ]) + texts = run[0].get("_ZeqR1AS0_", []) if run else [] + units: dict[str, dict[str, str]] = {} + for line in run[1] if run else []: + match = unit.match(line) + if match: + units.setdefault(match.group(1), {})[Path(match.group(2)).name] = match.group(3) + if name == "declared again": + ok = ( + len(texts) == 2 + and all("not checked with Clang:" in t for t in texts) + and all("b.cpp declares" in t for t in texts) + ) + else: + ok = len(texts) == 2 and all( + "checked in every file of the compilation database" in t for t in texts + ) + ok = ok and units == {"x": judged, "y": judged} + print(f" {'✅' if ok else '❌'} {name}" + f"{'' if ok else f': {texts}, units judged: {units}'}") + good = good and ok + print() + return good + + +def check_const_suggestion_identifier_starting_with_operator() -> bool: + """ + #217 (review of #239's fixes): a C function named by one identifier that starts with + `operator`, `operator$x` or `operatoré`, is not an operator: Clang's kind of the name, not its + spelling, tells them apart. b.c, which only the compilation database lists, declares it again + with its current type: b.c is judged, and the suggestion is kept, not checked, for that + declaration (D17). + """ + print("=== Testing a function whose identifier starts with operator ===") + good = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_operator_identifier_") as tmp: + root = Path(tmp).resolve() + for index, name in enumerate(("operator$x", "operatoré")): + case = root / f"case{index}" + case.mkdir() + (case / "a.c").write_text(f"int {name}(int *p) {{ return *p; }}\n", encoding="utf-8") + (case / "b.c").write_text( + f"int {name}(int *p);\nint use(int *q) {{ return {name}(q); }}\n", encoding="utf-8" + ) + (case / "compile_commands.json").write_text(json.dumps([ + {"directory": str(case), "command": f"clang -c {f} -o {f}.o", "file": f} + for f in ("a.c", "b.c") + ])) + run = _const_suggestion_run([ + str(case / "a.c"), f"--compile-commands={case / 'compile_commands.json'}" + ]) + texts = [text for found in run[0].values() for text in found] if run else [] + ok = ( + len(texts) == 1 + and "not checked with Clang:" in texts[0] + and f"b.c declares {name} with its current type" in texts[0] + ) + print(f" {'✅' if ok else '❌'} {name}{'' if ok else f': {texts}'}") + good = good and ok + print() + return good + + +def check_const_suggestion_declared_in_a_system_header() -> bool: + """ + #217 (review of #239's fixes): b, which only the compilation database lists, declares the + function again with its current type only through a header that its command includes with + -isystem; b's own text does not name it. That declaration counts as one b writes: b is judged, + and the suggestion is kept, not checked, for it (D17), for an operator and for a function + named by an identifier alike. The units judged must not leave system headers out. + """ + print("=== Testing a declaration made by a header included with -isystem ===") + good = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_system_header_") as tmp: + root = Path(tmp).resolve() + for name, extension, compiler, definition, declaration, function in ( + ("operator", "cpp", "clang++", + "struct A { int v; };\nbool operator==(A& x, A& y) { return x.v == y.v; }\n", + "struct A { int v; };\nbool operator==(A& x, A& y);\n", "_ZeqR1AS0_"), + ("identifier", "c", "clang", "int opx(int *p) { return *p; }\n", + "int opx(int *p);\n", "opx"), + ): + case = root / name + (case / "system").mkdir(parents=True) + (case / "system" / "declaration.h").write_text(declaration) + (case / f"a.{extension}").write_text(definition) + (case / f"b.{extension}").write_text("#include \nint other(void);\n") + (case / "compile_commands.json").write_text(json.dumps([ + {"directory": str(case), "file": f"a.{extension}", + "command": f"{compiler} -c a.{extension} -o a.o"}, + {"directory": str(case), "file": f"b.{extension}", + "command": f"{compiler} -isystem {case / 'system'} -c b.{extension} -o b.o"}, + ])) + run = _const_suggestion_run([ + str(case / f"a.{extension}"), f"--compile-commands={case / 'compile_commands.json'}" + ]) + texts = run[0].get(function, []) if run else [] + ok = bool(texts) and all( + "not checked with Clang:" in t and f"b.{extension} declares" in t for t in texts + ) + print(f" {'✅' if ok else '❌'} {name}{'' if ok else f': {texts}'}") + good = good and ok + print() + return good + + +def _llvm_clang() -> Optional[Path]: + """ + The clang++ of the LLVM that the analyzer under test is built with, found through its build's + CMakeCache.txt: the analyzer's Clang reads the precompiled headers and header units it writes. + """ + cache = RUN_CONFIG.analyzer.resolve().parent / "CMakeCache.txt" + found = re.search(r"^LLVM_DIR:\w+=(.+)$", cache.read_text(), re.MULTILINE) if cache.exists() else None + clang = Path(found.group(1)).resolve().parents[2] / "bin" / "clang++" if found else None + return clang if clang is not None and clang.exists() else None + + +def check_const_suggestion_module_import() -> bool: + """ + #239: a.cpp defines f. b.cpp, which only the compilation database lists, gets a declaration of + f and a use of it from pre.h, and never names f in its own text. The check tells whether b.cpp + sees f from its preprocessing, which does not read a module that b.cpp imports. When b.cpp + imports one, as a Clang header module (its #include of pre.h becomes an import), a C++20 header + unit, a C++20 named module, or the interface of the module whose implementation unit it is + (neither interface is built: preprocessing does not read it), the suggestion is not checked + for that import. The witness: b.cpp gets pre.h from a precompiled header, which preprocessing + replaces with pre.h itself, so b.cpp is judged, and the suggestion stays not checked for + b.cpp's declaration (D17). In every case, a.cpp's static s, which no other file can declare, + stays checked. Last, an import does not hide a refutation: c.cpp, listed after b.cpp, which + imports the header module, refutes the suggestion, and the trace shows b.cpp's import; b.cpp + refutes it too when it imports an unrelated module and includes a.cpp's rewritten header. + """ + print("=== Testing a file of the compilation database that imports a module ===") + clang = _llvm_clang() + if clang is None: + print(" ❌ the clang++ of the analyzer's LLVM is needed (LLVM_DIR in its CMakeCache.txt)") + return False + sysroot = [] + if sys.platform == "darwin": + sdk = subprocess.run(["xcrun", "--show-sdk-path"], capture_output=True, text=True) + sysroot = ["-isysroot", sdk.stdout.strip()] + main = "int main() { return g() - 1; }\n" + good = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_module_") as tmp: + root = Path(tmp).resolve() + + def analyze(case: Path, b_text: str, b_flags: str, built: list[str], c_text: str = ""): + case.mkdir() + (case / "a.h").write_text("int f(int *p);\n") + (case / "a.cpp").write_text( + '#include "a.h"\nint f(int *p) { return *p; }\n' + "static int s(int *p) { return *p; }\nint use_s() { int x = 1; return s(&x); }\n" + ) + (case / "pre.h").write_text("int f(int *p);\ninline int g() { int x = 1; return f(&x); }\n") + (case / "other.h").write_text("int other();\n") + (case / "module.modulemap").write_text( + 'module Pre {\n header "pre.h"\n export *\n}\n' + 'module Other {\n header "other.h"\n export *\n}\n' + ) + (case / "b.cpp").write_text(b_text) + if built: + result = subprocess.run([str(clang), *sysroot, *built], cwd=case, + capture_output=True, text=True) + if result.returncode != 0: + print(result.stderr) + return None + units = [("a.cpp", "-std=c++17"), ("b.cpp", b_flags)] + if c_text: + (case / "c.cpp").write_text(c_text) + units.append(("c.cpp", "-std=c++17")) + (case / "compile_commands.json").write_text(json.dumps([ + {"directory": str(case), "file": name, + "command": shlex.join(["clang++", *sysroot, *shlex.split(flags), "-c", name])} + for name, flags in units + ])) + return _const_suggestion_run([ + str(case / "a.cpp"), f"--compile-commands={case / 'compile_commands.json'}" + ]) + + def static_checked(run) -> bool: + texts = run[0].get("_ZL1sPi", []) if run else [] + return len(texts) == 1 and "checked in every file of the compilation database" in texts[0] + + def modules(case: str) -> str: + return f"-std=c++17 -fmodules -fimplicit-module-maps -fmodules-cache-path={root}/{case}/cache" + + for directory, name, b_text, b_flags, built, reason in ( + ("header", "Clang header module", '#include "pre.h"\n' + main, modules("header"), [], + "b.cpp imports a module"), + ("unit", "C++20 header unit", 'import "pre.h";\n' + main, + f"-std=c++20 -fmodule-file={root}/unit/pre.pcm", + ["-std=c++20", "-fmodule-header=user", "-xc++-header", "pre.h", "-o", "pre.pcm"], + "b.cpp imports a module"), + ("named", "C++20 named module", "import M;\n" + main, "-std=c++20", [], + "b.cpp imports a module"), + ("implementation", "C++20 module implementation unit", + "module M;\nint h() { return g() - 1; }\n", "-std=c++20", [], "b.cpp imports a module"), + ("pch", "precompiled header, the witness", main, + f"-std=c++17 -include-pch {root}/pch/pre.h.pch", + ["-std=c++17", "-xc++-header", "pre.h", "-o", "pre.h.pch"], "b.cpp declares f(int*)"), + ): + run = analyze(root / directory, b_text, b_flags, built) + texts = run[0].get("_Z1fPi", []) if run else [] + ok = (len(texts) == 1 and "not checked with Clang:" in texts[0] and reason in texts[0] + and static_checked(run)) + print(f" {'✅' if ok else '❌'} {name}{'' if ok else f': {run[0] if run else None}'}") + good = good and ok + + for directory, name, b_text, c_text, refuter, importer in ( + ("refuted", "refuted by c.cpp, b.cpp importing the module", '#include "pre.h"\n' + main, + '#include "a.h"\nint (*fp)(int *) = f;\n', "c.cpp:2", True), + ("importer", "refuted by b.cpp, which imports a module and reads the rewritten header", + '#include "other.h"\n#include "a.h"\nint (*fp)(int *) = f;\n', "", "b.cpp:3", False), + ): + case = root / directory + run = analyze(case, b_text, modules(directory), [], c_text) + messages, trace = (run[0], run[1]) if run else ({}, []) + ok = ( + "_Z1fPi" not in messages and static_checked(run) + and any(line.startswith(f"_Z1fPi parameter p: removed, does not compile: {case}/{refuter}:") + for line in trace) + and (not importer or any( + re.match(rf"_Z1fPi parameter p, unit {re.escape(str(case))}/b\.cpp \[[0-9a-f]+\]: " + rf"indeterminate: .*b\.cpp imports a module", line) for line in trace)) + ) + print(f" {'✅' if ok else '❌'} {name}{'' if ok else f': {messages}, trace: {trace}'}") + good = good and ok + print() + return good + + +def check_const_suggestion_defining_file_imports_a_module() -> bool: + """ + #239: a.cpp, alone in the compilation database, defines f and imports the Clang header module + Pre, whose header includes a.h and initializes an int (*)(int *) with f: the rewrite of f's + parameter breaks that initialization. a.cpp is compiled, so its compilation loads Pre, but the + check does not rebuild Pre with the rewritten a.h: Pre's declaration of f keeps its type, and + the suggestion stays not checked. The witness: without -fmodules, a.cpp reads pre.h as text, and + the rewrite is refuted at pre.h:2. + """ + print("=== Testing a defining file that imports a module ===") + sysroot = [] + if sys.platform == "darwin": + sdk = subprocess.run(["xcrun", "--show-sdk-path"], capture_output=True, text=True) + sysroot = ["-isysroot", sdk.stdout.strip()] + good = True + with tempfile.TemporaryDirectory(prefix="ct_const_suggestion_defining_module_") as tmp: + root = Path(tmp).resolve() + for directory, name, flags, expected in ( + ("module", "a.cpp imports Pre, whose header uses f", + f"-std=c++17 -fmodules -fimplicit-module-maps -fmodules-cache-path={root}/module/cache", + "not checked, the rewritten declaration does not get the suggested type"), + ("textual", "witness, a.cpp reads pre.h as text", "-std=c++17", + f"removed, does not compile: {root}/textual/pre.h:2: "), + ): + case = root / directory + case.mkdir() + (case / "a.h").write_text("int f(int *p);\n") + (case / "pre.h").write_text( + '#include "a.h"\ninline int call(int *p) { int (*g)(int *) = f; return g(p); }\n' + ) + (case / "module.modulemap").write_text('module Pre {\n header "pre.h"\n export *\n}\n') + (case / "a.cpp").write_text('#include "pre.h"\n#include "a.h"\nint f(int *p) { return *p; }\n') + (case / "compile_commands.json").write_text(json.dumps([ + {"directory": str(case), "file": "a.cpp", + "command": shlex.join(["clang++", *sysroot, *shlex.split(flags), "-c", "a.cpp"])} + ])) + run = _const_suggestion_run([ + str(case / "a.cpp"), f"--compile-commands={case / 'compile_commands.json'}" + ]) + trace = run[1] if run else [] + ok = any(line.startswith(f"_Z1fPi parameter p: {expected}") for line in trace) + print(f" {'✅' if ok else '❌'} {name}{'' if ok else f': {trace}'}") + good = good and ok + print() + return good + + def check_duplicate_if_cross_tu() -> bool: """ #157: a function defined in another file is deterministic when every definition of it is, as @@ -5562,6 +7502,7 @@ def check_file(c_path: Path): strict_diag_count, strict_details, unknown_scopes, + analyzer_args, ) = extract_expectations(c_path) if unknown_scopes: known = ", ".join(f"[{p}]" for p in _EXPECTATION_PASSES) @@ -5700,6 +7641,7 @@ def evaluate_pass(pass_name: str, analyzer_output: str): resource_model=resource_model, escape_model=escape_model, buffer_model=buffer_model, + extra_args=tuple(analyzer_args), ) # With runner-level --smt args this run is neither the default pass nor the smt-z3 one: # only unscoped expectations describe it. @@ -5721,6 +7663,7 @@ def evaluate_pass(pass_name: str, analyzer_output: str): escape_model=escape_model, buffer_model=buffer_model, extra_args=( + *analyzer_args, "--smt=on", "--smt-backend=z3", "--smt-mode=single", @@ -5803,6 +7746,43 @@ def record_ok(ok: bool): check_uninitialized_cross_tu_no_effect, check_const_param_cross_tu, check_const_param_contract_message_across_files, + check_const_suggestion_cross_tu, + check_const_suggestion_without_source, + check_const_suggestion_compilation_database, + check_const_suggestion_check_is_opt_in, + check_const_suggestion_line_endings, + check_const_suggestion_removal_reason, + check_const_suggestion_both_modes, + check_const_suggestion_one_configuration_per_file, + check_const_suggestion_compiles_each_file_once, + check_const_suggestion_operator_used_elsewhere, + check_const_suggestion_homonyms_across_files, + check_const_suggestion_header_read_differently, + check_const_suggestion_overload_defined_elsewhere, + check_const_suggestion_inline_in_a_header, + check_const_suggestion_merged_declaration, + check_const_suggestion_strong_and_weak_definitions, + check_const_suggestion_cast_in_the_defining_file, + check_const_suggestion_trace, + check_const_suggestion_werror_on_a_tracked_group, + check_const_suggestion_conditional_between_callbacks, + check_const_suggestion_extern_c_user, + check_const_suggestion_top_level_qualifier, + check_const_suggestion_deduced_type_error, + check_const_suggestion_written_part_outside_the_plan, + check_const_suggestion_default_argument_outside_the_plan, + check_const_suggestion_p2_lambdas_of_another_specialization, + check_const_suggestion_p2_local_class_in_a_library_template, + check_const_suggestion_p2_other_specialization_of_the_rewritten_template, + check_const_suggestion_p2_keys_keep_candidates_apart, + check_const_suggestion_p2_implicit_members_of_a_specialization, + check_const_suggestion_p2_owner_changed_by_the_rewrite, + check_const_suggestion_external_dependency_only_declared, + check_const_suggestion_operator_declared_elsewhere, + check_const_suggestion_identifier_starting_with_operator, + check_const_suggestion_declared_in_a_system_header, + check_const_suggestion_module_import, + check_const_suggestion_defining_file_imports_a_module, check_duplicate_if_cross_tu, check_global_read_before_write_identity_across_files, check_size_minus_one_cross_tu, diff --git a/src/StackUsageAnalyzer.cpp b/src/StackUsageAnalyzer.cpp index 07a956b..354aef0 100644 --- a/src/StackUsageAnalyzer.cpp +++ b/src/StackUsageAnalyzer.cpp @@ -3,6 +3,7 @@ #include "analyzer/AnalysisPipeline.hpp" #include "analyzer/HotspotProfiler.hpp" +#include "analysis/ConstSuggestionCheck.hpp" #include "analysis/InputPipeline.hpp" #include @@ -44,7 +45,9 @@ namespace ctrace::stack AnalysisResult result; { const analyzer::ScopedHotspot hotspot(config.timing, "analyze.pipeline"); - result = analyzeModule(*load.module, config); + AnalysisConfig unitConfig = config; + unitConfig.analyzedUnits = analysis::analyzedUnitOf(filename, *load.module); + result = analyzeModule(*load.module, unitConfig); } if (!load.frontendDiagnostics.empty()) { diff --git a/src/analysis/ConstSuggestionCheck.cpp b/src/analysis/ConstSuggestionCheck.cpp new file mode 100644 index 0000000..4ba3f80 --- /dev/null +++ b/src/analysis/ConstSuggestionCheck.cpp @@ -0,0 +1,1020 @@ +// SPDX-License-Identifier: Apache-2.0 +// Checks the const suggestions of ConstParameterNotModified with Clang (#217): the analysis of the +// IR decides whether an execution writes the pointee; Clang compiles every file that sees the +// change as it is and with the suggested type, and the two compilations decide whether the +// suggestion is verified, refuted or kept as not checked (§5.8). +#include "analysis/ConstSuggestionCheck.hpp" + +#include "ConstSuggestionInternal.hpp" +#include "analysis/AnalyzerUtils.hpp" +#include "analysis/CompileCommands.hpp" +#include "analysis/InputPipeline.hpp" +#include "analyzer/HotspotProfiler.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ctrace::stack::analysis +{ + namespace + { + using const_suggestion::DeclarationSite; + using const_suggestion::Diagnostic; + using const_suggestion::Edit; + using const_suggestion::Fact; + using const_suggestion::FactKey; + using const_suggestion::FactKind; + using const_suggestion::inspect; + using const_suggestion::Judgement; + using const_suggestion::realPath; + using const_suggestion::scan; + using const_suggestion::SourceLinkage; + using const_suggestion::SuggestionPlan; + using const_suggestion::Target; + using const_suggestion::UnitFacts; + using const_suggestion::Verdict; + + // What the check covers: the analyzed files, or every file of the compilation database. + constexpr const char* kCheckedAnalyzed = + "checked in the analyzed files only, with their compile commands: "; + constexpr const char* kCheckedDatabase = + "checked in every file of the compilation database that sees the change, with its " + "compile command: "; + // What it verifies (README, "Const parameter suggestions checked with Clang"). + constexpr const char* kCheckedResult = + "they compile with the suggested type, and their generated code matches, metadata " + "included, once debug information, diagnostic metadata, descriptive names and " + "renamed symbols are set aside"; + // For an rvalue reference, the const reference only (D12). + constexpr const char* kCheckedReference = + "they compile with the suggested const reference, and their generated code matches, " + "metadata included, once debug information, diagnostic metadata, descriptive names " + "and renamed symbols are set aside; passing by value is not checked"; + constexpr const char* kNotChecked = "not checked with Clang: "; + constexpr const char* kDoesNotCompileAsItIs = "a file does not compile with Clang as it is"; + + // A unit before the rewrite, prepared once for every suggestion of the module: what its + // compilation shows, its module released, and its generated code ready to compare. + struct Original + { + UnitFacts unit; + std::shared_ptr code; + }; + + Original originalFrom(UnitFacts unit, bool timing) + { + const analyzer::ScopedHotspot hotspot(timing, "const_suggestion.prepare"); + auto code = std::make_shared( + const_suggestion::prepare(*unit.module)); + unit.module.reset(); + unit.context.reset(); + return {std::move(unit), std::move(code)}; + } + + bool commandFor(const AnalyzedUnit& unit, const AnalysisConfig& config, UnitCommand& out, + std::string& error) + { + std::vector args; + if (!buildSourceCompileCommand(unit.filename, unit.language, config, args, + out.workingDir, error)) + { + return false; + } + // Options only compilerlib understands. + std::erase_if(args, [](const std::string& arg) { return arg.starts_with("--ct-"); }); + compilerlib::DriverConfig driver; + if (!compilerlib::resolveDriverConfig(args, driver, error)) + return false; + out.args = {driver.clang_path}; + if (driver.force_cxx_driver) + out.args.push_back("--driver-mode=g++"); + if (driver.add_resource_dir) + { + out.args.push_back("-resource-dir"); + out.args.push_back(driver.resource_dir); + } + if (driver.add_sysroot) + { + out.args.push_back("-isysroot"); + out.args.push_back(driver.sysroot); + } + out.args.insert(out.args.end(), args.begin(), args.end()); + return true; + } + + // An input as a real absolute path: relative inputs resolve from the input directory. + std::string canonicalPath(const std::string& filename, const AnalysisConfig& config) + { + llvm::SmallString<256> path(filename); + if (!llvm::sys::path::is_absolute(path)) + { + llvm::SmallString<256> base(config.inputBaseDir); + if (base.empty()) + llvm::sys::fs::current_path(base); + llvm::sys::path::append(base, filename); + path = base; + } + return realPath(path); + } + + bool references(const llvm::Module& module, llvm::StringRef symbol) + { + const llvm::GlobalValue* value = module.getNamedValue(symbol); + return value && !value->hasLocalLinkage(); + } + + } // namespace + + std::map const_suggestion::rewrite(const SuggestionPlan& plan, + std::string& failure) + { + std::map out; + for (const auto& [path, edits] : plan.edits) + { + llvm::ErrorOr> buffer = + llvm::MemoryBuffer::getFile(path); + if (!buffer) + { + failure = path + " cannot be read"; + return out; + } + std::string text = (*buffer)->getBuffer().str(); + std::vector sorted = edits; + std::sort(sorted.begin(), sorted.end(), + [](const Edit& a, const Edit& b) { return a.offset > b.offset; }); + for (const Edit& edit : sorted) + { + if (edit.offset > text.size() || edit.length > text.size() - edit.offset) + { + failure = path + " changed after the plan was made"; + return out; + } + text.replace(edit.offset, edit.length, edit.text); + } + out[path] = std::move(text); + } + return out; + } + + namespace + { + // A unit before the rewrite, by its file and its configuration: compiled once for every + // suggestion of the module (#217, §5, step 3). + using OriginalCache = std::map>; + + const std::optional& originalOf(const UnitCommand& command, + const std::string& key, + const std::vector& planned, + OriginalCache& cache, bool timing) + { + std::optional& out = + cache[key + '\0' + const_suggestion::fingerprintOf(command)]; + if (out) + return out; + UnitFacts before = inspect(command, nullptr, {}, planned, + "const_suggestion.compile.other_before", timing); + if (!before.failure.empty() || !before.module) + return out; + out = originalFrom(std::move(before), timing); + return out; + } + + // The reason a fact gives once the rewrite changes it (Texts). + std::string changeOf(const Fact& before, const Fact& after) + { + const std::string place = + before.key.site.expansion.file + ":" + std::to_string(before.line); + if (before.kind == FactKind::Reference) + return place + " refers to '" + after.shown + "' instead of '" + before.shown + "'"; + if (before.kind == FactKind::Override) + { + return "'" + before.subject + "' overrides " + after.shown + " instead of " + + before.shown; + } + if (before.subject == "_Generic") + return "the _Generic selection at " + place + " chooses another association"; + return "the condition at " + place + " becomes " + after.value.front(); + } + + // What step 6 finds: the first fact that the rewrite changes, and else, when a site's + // facts cannot be compared one to one, why the comparison is incomplete. + struct FactComparison + { + std::optional changed; + std::optional incomplete; + }; + + // Step 6 (§5.8): the facts R and K, then O, compared key by key, one to one in the order + // of the traversal: one key can hold several facts, such as a macro argument expanded + // twice. A key with as many facts on both sides is compared completely; one with a + // different number, a fact on one side only included, leaves the comparison incomplete. + // Keys go through the entity's own symbol pairs: a fact in an instantiation they do not + // map is left out, and so is a reference whose targets are specializations or implicit + // functions on both sides; Task 1.5 pairs them. + FactComparison compareFacts(const UnitFacts& before, std::size_t index, + const UnitFacts& after, const std::vector& pairs) + { + std::map renamed; // the established pairs, before to after + std::map back; + for (const SymbolPair& pair : pairs) + { + renamed[pair.before] = pair.after; + back[pair.after] = pair.before; + } + const UnitFacts::Entity* entity = after.entities.empty() ? nullptr : &after.entities[0]; + // A key before the rewrite, as the compilation after it writes it. + const auto forward = [&](FactKey key) -> std::optional + { + for (std::string& context : key.contexts) + { + const auto pair = renamed.find(context); + if (pair != renamed.end()) + context = pair->second; + else if (!after.instantiations.count(context)) + return std::nullopt; + } + return key; + }; + // Whether a key after the rewrite can have a counterpart before it. + const auto known = [&](const FactKey& key) + { + return std::all_of( + key.contexts.begin(), key.contexts.end(), [&](const std::string& context) + { return back.count(context) || before.instantiations.count(context); }); + }; + const auto mappedBack = [&](std::vector value) + { + for (std::string& item : value) + { + const auto pair = + item.starts_with("symbol:") ? back.find(item.substr(7)) : back.end(); + if (pair != back.end()) + item = "symbol:" + pair->second; + } + std::sort(value.begin(), value.end()); + return value; + }; + const auto sites = [](const std::vector& value) + { + return std::all_of(value.begin(), value.end(), [](const std::string& item) + { return item.starts_with("site:"); }); + }; + const auto changed = [&](const Fact& was, const Fact& now) + { + if (was.kind != FactKind::Reference) + return mappedBack(now.value) != was.value; + // Unchanged when its targets after the rewrite keep every declaration they had + // (ruling 4). + if (sites(was.value) && sites(now.value)) + { + return !std::includes(now.value.begin(), now.value.end(), was.value.begin(), + was.value.end()); + } + if (sites(was.value) != sites(now.value)) + return true; + // Two symbols: the same target through the pairs; else a change only between two + // templates, or between a specialization and a function of no template. Two + // specializations of one template are the correspondences' to pair (§5.7). + if (mappedBack(now.value) == was.value) + return false; + const std::string wasTemplate = const_suggestion::itemOf(was.value, "template:"); + const std::string nowTemplate = const_suggestion::itemOf(now.value, "template:"); + return wasTemplate.empty() != nowTemplate.empty() || wasTemplate != nowTemplate; + }; + FactComparison out; + const auto compare = [&](const std::vector& was, const std::vector& now) + { + std::vector order; // as the traversal first meets each key + std::map, std::vector>> at; + const auto add = [&](const FactKey& key, const Fact& fact, bool rewritten) + { + auto [it, added] = at.try_emplace(key); + if (added) + order.push_back(key); + (rewritten ? it->second.second : it->second.first).push_back(&fact); + }; + for (const Fact& fact : was) + { + if (const std::optional key = forward(fact.key)) + add(*key, fact, false); + } + for (const Fact& fact : now) + { + if (known(fact.key)) + add(fact.key, fact, true); + } + for (const FactKey& key : order) + { + const auto& [wasAt, nowAt] = at.at(key); + if (wasAt.size() != nowAt.size()) + { + const Fact& one = wasAt.empty() ? *nowAt.front() : *wasAt.front(); + if (!out.incomplete) + { + out.incomplete = + "the facts at " + one.key.site.expansion.file + ":" + + std::to_string(one.line) + + " cannot be compared one to one: " + std::to_string(wasAt.size()) + + " before the rewrite, " + std::to_string(nowAt.size()) + + " after it"; + } + continue; + } + for (std::size_t k = 0; k < wasAt.size(); ++k) + { + if (changed(*wasAt[k], *nowAt[k])) + { + out.changed = changeOf(*wasAt[k], *nowAt[k]); + return; + } + } + } + }; + compare(before.facts, after.facts); + if (!out.changed && index < before.entities.size()) + { + compare(before.entities[index].overrides, + entity ? entity->overrides : std::vector()); + } + return out; + } + + // Step 3 (§5.6): a declaration that a rewritten file writes outside the plan and whose + // type the rewrite changes, valid or not after it; and else, when a site's declarations + // cannot be compared one to one, why the comparison is incomplete. + std::optional outsideThePlan(const UnitFacts& before, const UnitFacts& after, + const SuggestionPlan& plan) + { + using Declarations = std::vector; + const auto planned = [&](const DeclarationSite& site) + { + return !plan.edits.count(site.expansion.file) || + std::binary_search(plan.entity.redeclarations.begin(), + plan.entity.redeclarations.end(), site) || + std::binary_search(plan.parameters.begin(), plan.parameters.end(), site); + }; + std::optional incomplete; + const Declarations none; + const auto compare = [&](const DeclarationSite& site, const Declarations& was, + const Declarations& now) -> std::optional + { + if (was.size() != now.size()) + { + const UnitFacts::Written& one = was.empty() ? now.front() : was.front(); + if (!incomplete) + { + incomplete = "the declarations at " + site.expansion.file + ":" + + std::to_string(one.line) + " cannot be compared one to one: " + + std::to_string(was.size()) + " before the rewrite, " + + std::to_string(now.size()) + " after it"; + } + return std::nullopt; + } + for (std::size_t k = 0; k < was.size(); ++k) + { + if (was[k].type != now[k].type) + { + return "the rewrite also changes the type of " + now[k].name + " (" + + site.expansion.file + ":" + std::to_string(now[k].line) + ")"; + } + } + return std::nullopt; + }; + for (const auto& [site, now] : after.writtenTypes) + { + if (planned(site)) + continue; + const auto was = before.writtenTypes.find(site); + if (std::optional changed = + compare(site, was == before.writtenTypes.end() ? none : was->second, now)) + { + return changed; + } + } + for (const auto& [site, was] : before.writtenTypes) + { + if (planned(site) || after.writtenTypes.count(site)) + continue; + if (std::optional changed = compare(site, was, none)) + return changed; + } + // A type that an edit writes outside the planned parameters' types (D19). + if (after.writtenOutside) + { + return "the rewrite also changes the type written at " + + after.writtenOutside->site.expansion.file + ":" + + std::to_string(after.writtenOutside->line); + } + return incomplete; + } + + Judgement refuted(std::string why) + { + return {std::move(why), Verdict::Refuted}; + } + + Judgement indeterminate(std::string why) + { + return {std::move(why), Verdict::Indeterminate}; + } + + // The rule of a suggestion, as its diagnostic names it. + std::string ruleOf(const ConstParamIssue& issue) + { + const char* rule = "Pointer"; + if (issue.pointerConstOnly) + rule = "PointerConstOnly"; + else if (issue.isReference) + rule = issue.isRvalueRef ? "ReferenceRvaluePreferValue" : "Reference"; + return std::string("ConstParameterNotModified.") + rule; + } + + // Checks one suggestion, the target at `index`, in one unit, against the unit before the + // rewrite: the judgement of §5.8. `after` receives the unit's compilation after the + // rewrite, with the collisions it finds (§5.10); `definingFile` names the defining unit. + Judgement checkUnit(const UnitCommand& command, const std::optional& before, + std::size_t index, const Target& planned, + const std::map& rewritten, + const SuggestionPlan& plan, const std::string& definingFile, + PrintedSignature& signature, std::vector& accepted, + std::string& renamed, UnitFacts& after, bool timing) + { + // Step 1: a unit that cannot be checked. + if (!before) + return indeterminate(kDoesNotCompileAsItIs); + // Each declaration's candidate, as the unit writes it before the rewrite (D8, D18). + Target target = planned; + target.expected = before->unit.entities[index].expected; + after = inspect(command, &plan, rewritten, {target}, "const_suggestion.compile.after", + timing); + if (!after.failure.empty()) + return indeterminate(after.failure); + // Step 2: after a fatal error, the rest of the unit is not inspected. + if (!after.fatal.empty()) + { + return indeterminate("the rewritten code cannot be inspected entirely, a fatal " + "error stops its compilation: " + + after.fatal); + } + const UnitFacts::Entity& facts = + after.entities.empty() ? UnitFacts::Entity() : after.entities[0]; + // Step 3, the plan's scope (§5.6): its parameters have the candidate, and every other + // declaration that a rewritten file writes keeps its type. + if (facts.withoutCandidate) + return indeterminate("the rewritten declaration does not get the suggested type"); + if (const std::optional outside = + outsideThePlan(before->unit, after, plan)) + return indeterminate(*outside); + // Steps 4 and 5: the first error or checked warning that the rewrite adds. + for (const Diagnostic& diagnostic : after.reported) + { + if (after.diagnostics.count(diagnostic.key) > + before->unit.diagnostics.count(diagnostic.key)) + { + return refuted( + (diagnostic.error ? "does not compile: " : "gains a checked warning: ") + + diagnostic.text); + } + } + // A rewrite that does not compile, and adds no error to tell why: it is not shown to be + // wrong (#239 review, M6). The reason names its first error, not an earlier warning. + if (!after.module) + { + const auto error = + std::find_if(after.reported.begin(), after.reported.end(), + [](const Diagnostic& diagnostic) { return diagnostic.error; }); + return indeterminate( + "the rewritten code does not compile, and no error that the rewrite adds " + "explains it" + + (error == after.reported.end() ? "" : ": " + error->text)); + } + // Another entity's declaration that the rewrite merges into the entity: a declaration + // it has after the rewrite only (ruling 3). + if (!after.entities.empty() && index < before->unit.entities.size()) + { + UnitFacts::Entity& entity = after.entities[0]; + for (const auto& [site, line] : entity.declarations) + { + if (!before->unit.entities[index].declarations.count(site)) + { + entity.collisions.push_back( + {entity.variants.empty() ? planned.symbol : entity.variants[0].symbol, + site, std::nullopt, line}); + } + } + } + // The correspondences of the rewrite (§5.7), from the facts of both compilations; the + // ABI line names the suggestion's own symbol after it. + std::string missing; + const std::vector pairs = + const_suggestion::correspond(before->unit, index, after, missing); + for (const SymbolPair& pair : pairs) + { + if (renamed.empty() && pair.before == planned.symbol) + renamed = pair.after; + } + // Step 6: a reference, an override or a constant condition that changes, compared + // through the pairs; facts that cannot be compared one to one leave the unit + // unverified, and so does a correspondence that is not one to one. + const FactComparison compared = compareFacts(before->unit, index, after, pairs); + if (compared.changed) + return refuted(*compared.changed); + if (compared.incomplete) + return indeterminate(*compared.incomplete); + // A unit that declares the entity for the linker only, with its current type: whether + // that declaration matters depends on how the files are linked (D17). + if (facts.stale) + { + return indeterminate(after.file + " declares " + + formatFunctionNameForMessage(planned.symbol) + + " with its current type and does not see the rewrite: if " + + definingFile + " and " + after.file + + " are linked together, that declaration no longer matches " + "the rewritten definition"); + } + // An explicit cast of its address, other than to void: what it hides may change with + // the rewrite, and neither the errors nor the facts tell (D15). Alone, it leaves the + // unit unverified; the steps above still refute. + if (!facts.casts.empty()) + { + const UnitFacts::Entity::Cast& cast = facts.casts.front(); + return indeterminate(cast.site.expansion.file + ":" + std::to_string(cast.line) + + " converts the address of " + + formatFunctionNameForMessage(planned.symbol) + + " with an explicit cast"); + } + if (!missing.empty()) + return indeterminate(missing); + if (signature.after.empty() && !facts.printedSignature.empty()) + signature.after = facts.printedSignature; + // Step 7: the generated code, compared through the pairs. A difference that no + // reference, override or constant condition explains leaves the unit unverified: it + // never refutes. + std::optional difference; + { + const analyzer::ScopedHotspot hotspot(timing, "const_suggestion.compare"); + const const_suggestion::ModuleText now = const_suggestion::prepare(*after.module); + // A renamed symbol that this file only declares, on both sides, other than the + // entity's own variants and inheriting constructors (which the defining file + // compiles): comparing its two declarations proves nothing about the code that + // another file defines for it, if any does. + std::set own; + if (index < before->unit.entities.size()) + { + for (const const_suggestion::Named& named : + before->unit.entities[index].variants) + own.insert(named.symbol); + for (const const_suggestion::Named& named : + before->unit.entities[index].inheriting) + own.insert(named.symbol); + } + for (const SymbolPair& pair : pairs) + { + if (own.count(pair.before) || !before->code->globals.count(pair.before) || + !now.globals.count(pair.after) || + before->code->defined.count(pair.before) || now.defined.count(pair.after)) + { + continue; + } + return indeterminate(formatFunctionNameForMessage(pair.before) + " becomes " + + formatFunctionNameForMessage(pair.after) + ", which " + + after.file + + " only declares: the code another file defines for it " + "is not checked"); + } + difference = const_suggestion::codeDifference(*before->code, now, pairs, &signature, + &accepted); + } + if (difference) + { + return indeterminate("the generated code changes, and no reference, override or " + "constant condition explains it: " + + *difference); + } + return {}; + } + } // namespace + + std::string const_suggestion::fingerprintOf(const UnitCommand& command) + { + std::string text; + for (const std::string& arg : command.args) + text.append(arg).push_back('\0'); + text.append(command.workingDir).push_back('\0'); + return llvm::utohexstr(llvm::xxh3_64bits(text), /*LowerCase=*/true, /*Width=*/16); + } + + std::string const_suggestion::realPath(llvm::StringRef path) + { + llvm::SmallString<256> out; + if (llvm::sys::fs::real_path(path, out)) + return path.str(); + return out.str().str(); + } + + Judgement const_suggestion::aggregate(const std::vector& judgements) + { + for (const Verdict verdict : {Verdict::Refuted, Verdict::Indeterminate}) + { + for (const Judgement& judgement : judgements) + { + if (judgement.verdict == verdict) + return judgement; + } + } + return {}; + } + + std::shared_ptr analyzedUnitOf(const std::string& filename, + const llvm::Module& module) + { + auto units = std::make_shared(); + units->units.push_back({filename, &module, detectFromExtension(filename)}); + return units; + } + + void checkConstSuggestions(const llvm::Module& mod, std::vector& issues, + const AnalysisConfig& config) + { + if (issues.empty() || config.warningsOnly || !config.constSuggestionCheck) + return; + const bool timing = config.timing; + const analyzer::ScopedHotspot total(timing, "const_suggestion.total"); + const AnalyzedUnits* units = config.analyzedUnits.get(); + const AnalyzedUnit* self = nullptr; + for (std::size_t i = 0; units && i < units->units.size(); ++i) + { + if (units->units[i].module == &mod) + self = &units->units[i]; + } + const auto trace = [](const ConstParamIssue& issue, const std::string& outcome) + { + coretrace::log(coretrace::Level::Debug, "[const-suggestion] {} parameter {}: {}\n", + issue.funcName, issue.binding.name, outcome); + }; + // The verdict's grounds, after T v1's line (§4): the suggestion, the type it tries, where + // the rewrite writes it, each unit judged with its configuration, then the verdict. A + // suggestion whose plan is not executed has no rewrite and no unit. + const auto grounds = [](const ConstParamIssue& issue, const SuggestionPlan* plan, + const std::vector& judged, + const std::vector& judgements, const Judgement& verdict) + { + static constexpr const char* kVerdicts[] = {"verified", "refuted", "indeterminate"}; + static const std::map> kNoEdits; + const auto line = [&](const std::string& text) + { + coretrace::log(coretrace::Level::Debug, "[const-suggestion] {} parameter {}, {}\n", + issue.funcName, issue.binding.name, text); + }; + const auto outcome = [](const Judgement& judgement) + { + return kVerdicts[static_cast(judgement.verdict)] + + (judgement.reason.empty() ? std::string() : ": " + judgement.reason); + }; + line("suggestion: " + ruleOf(issue) + " on " + + formatFunctionNameForMessage(issue.funcName) + ", source index " + + (issue.binding.sourceIndex ? std::to_string(*issue.binding.sourceIndex) : "none")); + if (plan && !plan->candidate.type.empty()) + { + line("transformation: " + plan->candidate.current + " becomes " + + plan->candidate.type + " (" + plan->candidate.alternative + ")"); + } + for (const auto& [path, edits] : plan && !plan->indeterminate ? plan->edits : kNoEdits) + { + std::vector sorted = edits; + std::sort(sorted.begin(), sorted.end(), + [](const Edit& a, const Edit& b) { return a.offset < b.offset; }); + for (const Edit& edit : sorted) + line("rewritten at: " + path + ":" + std::to_string(edit.line)); + } + for (std::size_t k = 0; k < judgements.size(); ++k) + line("unit " + judged[k] + ": " + outcome(judgements[k])); + line("verdict: " + outcome(verdict)); + }; + const auto label = [](const std::string& file, const UnitCommand* command) + { + return file + " [" + (command ? const_suggestion::fingerprintOf(*command) : "none") + + "]"; + }; + const auto notChecked = [&](const std::string& why) + { + for (ConstParamIssue& issue : issues) + { + issue.checkLines.push_back(kNotChecked + why); + trace(issue, "not checked, " + why); + grounds(issue, nullptr, {}, {}, indeterminate(why)); + } + }; + if (!self || (self->language != LanguageType::C && self->language != LanguageType::CXX)) + { + notChecked("no source file, so whether the suggested type compiles was not verified"); + return; + } + UnitCommand command; + std::string error; + if (!commandFor(*self, config, command, error)) + { + notChecked("no compile command (" + error + ")"); + return; + } + + // Before the rewrite, the defining unit: one compilation plans every suggestion. + std::vector targets(issues.size()); + for (std::size_t i = 0; i < issues.size(); ++i) + { + targets[i].symbol = issues[i].funcName; + if (!issues[i].binding.isArtificial) + targets[i].sourceIndex = issues[i].binding.sourceIndex; + } + UnitFacts inspected = inspect(command, nullptr, {}, targets, + "const_suggestion.compile.defining_before", timing); + if (!inspected.failure.empty() || !inspected.module || + inspected.entities.size() != issues.size()) + { + notChecked("the file does not compile with Clang as it is"); + return; + } + const std::optional definingBefore = originalFrom(std::move(inspected), timing); + const UnitFacts& before = definingBefore->unit; + // The suggestions as planned: what the other compilations look for. + for (std::size_t i = 0; i < issues.size(); ++i) + targets[i].plan = &before.entities[i].plan; + OriginalCache originals; + + // The other units a rewrite may reach: the analyzed ones and, with a compilation + // database, every C or C++ file it lists. + std::vector candidates; + std::set analyzed; + for (const AnalyzedUnit& unit : units->units) + { + analyzed.insert(canonicalPath(unit.filename, config)); + if (&unit != self) + candidates.push_back(unit); + } + if (config.compilationDatabase) + { + for (const std::string& file : config.compilationDatabase->listSourceFiles()) + { + const LanguageType language = detectFromExtension(file); + if ((language == LanguageType::C || language == LanguageType::CXX) && + !analyzed.count(canonicalPath(file, config))) + { + candidates.push_back({file, nullptr, language}); + } + } + } + + std::vector kept; + for (std::size_t i = 0; i < issues.size(); ++i) + { + ConstParamIssue& issue = issues[i]; + const UnitFacts::Entity& found = before.entities[i]; + const SuggestionPlan& plan = found.plan; + // The type displayed is the plan's candidate, not the rule's proposal (stage 2, + // ruling 3): for an rvalue reference, the const reference alone. + if (!plan.candidate.shown.empty() && (issue.isRvalueRef || issue.pointerConstOnly)) + { + issue.suggestedType = plan.candidate.shown; + issue.suggestedTypeAlt.clear(); + } + if (!found.found && !plan.indeterminate) + { + issue.checkLines.push_back(kNotChecked + + std::string("the function is not found in the source")); + trace(issue, "not checked, the function is not found in the source"); + grounds(issue, &plan, {}, {}, + indeterminate("the function is not found in the source")); + kept.push_back(std::move(issue)); + continue; + } + if (plan.indeterminate) + { + issue.checkLines.push_back(kNotChecked + *plan.indeterminate); + trace(issue, "not checked, " + *plan.indeterminate); + grounds(issue, &plan, {}, {}, indeterminate(*plan.indeterminate)); + kept.push_back(std::move(issue)); + continue; + } + std::string unreadable; + const std::map rewritten = + const_suggestion::rewrite(plan, unreadable); + if (!unreadable.empty()) + { + issue.checkLines.push_back(kNotChecked + unreadable); + trace(issue, "not checked, " + unreadable); + grounds(issue, &plan, {}, {}, indeterminate(unreadable)); + kept.push_back(std::move(issue)); + continue; + } + std::set rewrittenFiles; + for (const auto& [path, text] : rewritten) + rewrittenFiles.insert(path); + + // The defining unit first: it gives the function's symbol after the rewrite. + const Target& planned = targets[i]; + PrintedSignature signature{found.printedSignature, ""}; + std::vector accepted; + std::string renamed; + // Each unit judged, as its compilation after the rewrite shows it: its collisions + // are judged once none of the units refutes (§5.10). Its module is not kept. + std::vector inspected(1); + const auto release = [](UnitFacts& unit) + { + unit.module.reset(); + unit.context.reset(); + }; + const std::string definingFile = canonicalPath(self->filename, config); + std::vector judgements{ + checkUnit(command, definingBefore, i, planned, rewritten, plan, definingFile, + signature, accepted, renamed, inspected.back(), timing)}; + release(inspected.back()); + std::vector judged{label(definingFile, &command)}; + // The other units look for the symbols the entity takes after the rewrite, as the + // defining unit names them. + Target others = planned; + if (!inspected.back().entities.empty()) + { + for (const const_suggestion::Named& variant : inspected.back().entities[0].variants) + others.takes.push_back(variant.symbol); + } + // Then every other unit that sees a rewritten file or references the function: the + // analyzed ones and, with a compilation database, every file it lists, analyzed or + // not. One that is not verified, the defining one included, leaves the suggestion + // unchecked, unless another one refutes it: only a refutation stops the judgement + // (§5.10). + const auto cannotCheck = + [&](const std::string& unit, const UnitCommand* known, const std::string& why) + { + judged.push_back(label(unit, known)); + judgements.push_back(indeterminate(why)); + }; + for (const AnalyzedUnit& unit : candidates) + { + if (judgements.back().verdict == Verdict::Refuted) + break; + // Another file references the function only through the linker: one with + // internal linkage is another file's homonym (§5.2). + const bool referencing = plan.entity.linkage == SourceLinkage::External && + unit.module && references(*unit.module, issue.funcName); + if (unit.language != LanguageType::C && unit.language != LanguageType::CXX) + { + if (referencing) + { + cannotCheck(canonicalPath(unit.filename, config), nullptr, + unit.filename + " uses the function but has no source"); + } + continue; + } + const std::string key = canonicalPath(unit.filename, config); + std::optional known; + bool resolved = false; + { + std::lock_guard lock(units->scansMutex); + const auto found = units->commands.find(key); + if (found != units->commands.end()) + { + known = found->second; + resolved = true; + } + } + if (!resolved) + { + UnitCommand command; + if (commandFor(unit, config, command, error)) + known = std::move(command); + std::lock_guard lock(units->scansMutex); + units->commands[key] = known; + } + if (!known) + { + cannotCheck(key, nullptr, "no compile command for " + unit.filename); + continue; + } + const UnitCommand& other = *known; + UnitScan seen; + bool cached = false; + { + std::lock_guard lock(units->scansMutex); + const auto found = units->scans.find(key); + if (found != units->scans.end()) + { + seen = found->second; + cached = true; + } + } + if (!cached) + { + seen = scan(other, timing); + std::lock_guard lock(units->scansMutex); + units->scans[key] = seen; + } + if (seen.files.empty()) + { + cannotCheck(key, &other, unit.filename + " cannot be preprocessed with Clang"); + continue; + } + const bool sees = + referencing || + std::any_of(rewrittenFiles.begin(), rewrittenFiles.end(), + [&](const std::string& path) { return seen.files.count(path); }) || + (plan.declarableElsewhere && + seen.identifiers.count(llvm::xxh3_64bits( + plan.entity.operatorName ? llvm::StringRef("operator") + : llvm::StringRef(plan.entity.name))) != 0); + if (!sees) + { + // The scan does not read a module that the unit imports: the unit may see + // the function there, unless only the defining file can declare it. + if (seen.importsModule && + (plan.declarableElsewhere || + std::any_of(rewrittenFiles.begin(), rewrittenFiles.end(), + [&](const std::string& path) + { return path != definingFile; }))) + { + cannotCheck(key, &other, + unit.filename + " imports a module, whose declarations the " + "check does not see"); + } + continue; + } + std::vector otherAccepted; + Judgement judgement = + checkUnit(other, originalOf(other, key, targets, originals, timing), i, others, + rewritten, plan, definingFile, signature, otherAccepted, renamed, + inspected.emplace_back(), timing); + release(inspected.back()); + if (judgement.verdict == Verdict::Indeterminate && + judgement.reason == kDoesNotCompileAsItIs) + { + judgement.reason = unit.filename + " does not compile with Clang as it is"; + } + judged.push_back(label(key, &other)); + judgements.push_back(std::move(judgement)); + for (const std::string& line : otherAccepted) + { + if (std::find(accepted.begin(), accepted.end(), line) == accepted.end()) + accepted.push_back(line); + } + } + Judgement verdict = const_suggestion::aggregate(judgements); + if (verdict.verdict == Verdict::Verified) + { + std::vector units; + for (const UnitFacts& unit : inspected) + units.push_back(&unit); + verdict = const_suggestion::judgeCollisions( + units, 0, definingFile, formatFunctionNameForMessage(issue.funcName)); + } + if (verdict.verdict == Verdict::Refuted) + { + trace(issue, "removed, " + verdict.reason); + grounds(issue, &plan, judged, judgements, verdict); + continue; + } + if (verdict.verdict == Verdict::Indeterminate) + { + issue.checkLines.push_back(kNotChecked + verdict.reason); + trace(issue, "not checked, " + verdict.reason); + grounds(issue, &plan, judged, judgements, verdict); + kept.push_back(std::move(issue)); + continue; + } + trace(issue, "checked in " + std::to_string(judgements.size()) + " file(s)"); + grounds(issue, &plan, judged, judgements, verdict); + const std::string checked = + std::string(config.compilationDatabase ? kCheckedDatabase : kCheckedAnalyzed) + + (issue.isRvalueRef ? kCheckedReference : kCheckedResult); + issue.checkLines.push_back( + accepted.empty() ? checked : checked + ", except for the differences below"); + issue.checkLines.insert(issue.checkLines.end(), accepted.begin(), accepted.end()); + const llvm::Function* function = mod.getFunction(issue.funcName); + if (function && function->hasExternalLinkage() && !renamed.empty() && + renamed != issue.funcName) + { + issue.checkLines.push_back("ABI: the suggested type changes the symbol (" + + issue.funcName + " -> " + renamed + + "): code compiled separately must use the updated " + "declaration and be recompiled"); + } + kept.push_back(std::move(issue)); + } + issues = std::move(kept); + } +} // namespace ctrace::stack::analysis diff --git a/src/analysis/ConstSuggestionCode.cpp b/src/analysis/ConstSuggestionCode.cpp new file mode 100644 index 0000000..66c3327 --- /dev/null +++ b/src/analysis/ConstSuggestionCode.cpp @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: Apache-2.0 +// Comparison of the code generated before and after a const rewrite (#217). +#include "analysis/ConstSuggestionCheck.hpp" +#include "ConstSuggestionInternal.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ctrace::stack::analysis +{ + namespace + { + bool isSymbolChar(char c) + { + return std::isalnum(static_cast(c)) || c == '-' || c == '$' || + c == '.' || c == '_'; + } + + // The symbol written at text[at] ('@' or '$' excluded), quoted or not, and its length. + std::pair symbolAt(const std::string& text, std::size_t at) + { + if (at < text.size() && text[at] == '"') + { + const std::size_t end = text.find('"', at + 1); + if (end == std::string::npos) + return {"", 0}; + return {text.substr(at + 1, end - at - 1), end - at + 1}; + } + std::size_t end = at; + while (end < text.size() && isSymbolChar(text[end])) + ++end; + return {text.substr(at, end - at), end - at}; + } + + std::string spellSymbol(const std::string& name) + { + const bool plain = !name.empty() && + std::all_of(name.begin(), name.end(), isSymbolChar) && + !std::isdigit(static_cast(name.front())); + return plain ? name : "\"" + name + "\""; + } + + // A printed line without its comment: the predecessors a block label lists follow the + // order of uses, which copying a module changes, and say nothing an execution does. + llvm::StringRef withoutComment(llvm::StringRef line) + { + bool quoted = false; + for (std::size_t i = 0; i < line.size(); ++i) + { + if (line[i] == '"') + quoted = !quoted; + else if (line[i] == ';' && !quoted) + return line.take_front(i).rtrim(); + } + return line; + } + + using const_suggestion::ModuleText; + + // Removes what LLVM says carries no meaning for an execution, so that every other + // metadata can be compared by contents: + // - debug information (StripDebugInfo, which also removes heapallocsite, a debug type, + // and the debug locations of loop identifiers); + // - srcloc, the source position of inline assembly and dontcall diagnostics; + // - annotation, which "does not impact the semantics of the program"; + // - the descriptive string of an alias scope or domain identified by a self-reference. + void removeNonSemanticMetadata(llvm::Module& module) + { + llvm::StripDebugInfo(module); + llvm::LLVMContext& context = module.getContext(); + const unsigned srcloc = context.getMDKindID("srcloc"); + llvm::MDBuilder builder(context); + std::map unnamed; + const auto selfNamed = [](const llvm::MDNode* node) + { return node->getNumOperands() > 0 && node->getOperand(0) == node; }; + std::function domain = [&](llvm::MDNode* node) + { + if (!selfNamed(node)) + return node; + llvm::MDNode*& out = unnamed[node]; + if (!out) + out = builder.createAnonymousAliasScopeDomain(); + return out; + }; + const auto scope = [&](llvm::MDNode* node) + { + auto* parent = node->getNumOperands() > 1 + ? llvm::dyn_cast(node->getOperand(1).get()) + : nullptr; + if (!selfNamed(node) || !parent) + return node; + llvm::MDNode*& out = unnamed[node]; + if (!out) + out = builder.createAnonymousAliasScope(domain(parent)); + return out; + }; + const auto list = [&](const llvm::MDNode* node) + { + llvm::SmallVector scopes; + for (const llvm::MDOperand& operand : node->operands()) + { + auto* member = llvm::dyn_cast_or_null(operand.get()); + scopes.push_back(member ? scope(member) : operand.get()); + } + return llvm::MDNode::get(context, scopes); + }; + for (llvm::Function& function : module) + { + for (llvm::Instruction& instruction : llvm::instructions(function)) + { + instruction.setMetadata(srcloc, nullptr); + instruction.setMetadata(llvm::LLVMContext::MD_annotation, nullptr); + for (const unsigned kind : + {llvm::LLVMContext::MD_alias_scope, llvm::LLVMContext::MD_noalias}) + { + if (llvm::MDNode* scopes = instruction.getMetadata(kind)) + instruction.setMetadata(kind, list(scopes)); + } + auto* declaration = llvm::dyn_cast(&instruction); + if (declaration && declaration->getIntrinsicID() == + llvm::Intrinsic::experimental_noalias_scope_decl) + { + auto* operand = + llvm::cast(declaration->getArgOperand(0)); + if (auto* scopes = llvm::dyn_cast(operand->getMetadata())) + { + declaration->setArgOperand( + 0, llvm::MetadataAsValue::get(context, list(scopes))); + } + } + } + } + } + + ModuleText textOf(llvm::Module& module) + { + removeNonSemanticMetadata(module); + std::string printed; + llvm::raw_string_ostream os(printed); + module.print(os, nullptr); + os.flush(); + + ModuleText out; + std::string current; + std::string body; + for (llvm::StringRef printedLine : llvm::split(printed, '\n')) + { + const llvm::StringRef line = withoutComment(printedLine); + if (!current.empty()) + { + body += line.str() + "\n"; + if (line == "}") + { + out.globals[current] = body; + current.clear(); + } + continue; + } + // Metadata: compared by contents where they are used, named ones apart. + if (line.starts_with("!")) + { + const std::size_t equal = line.find(" = "); + if (equal != llvm::StringRef::npos) + { + const std::string name = line.substr(0, equal).str(); + const bool numbered = + name.size() > 1 && std::isdigit(static_cast(name[1])); + (numbered ? out.metadata : out.namedMetadata)[name] = + line.substr(equal + 3).str(); + } + continue; + } + // Comdat selections are left out: each definition names its comdat, renamed and + // compared with it. + if (line.empty() || line.starts_with(";") || line.starts_with("$") || + line.starts_with("source_filename") || line.starts_with("target ")) + { + continue; + } + if (line.starts_with("attributes #")) + { + const std::size_t equal = line.find(" = "); + out.attributeGroups[line.substr(11, equal - 11).str()] = + line.substr(equal + 3).str(); + continue; + } + if (line.starts_with("%")) + { + const std::size_t equal = line.find(" = "); + out.types[line.substr(0, equal).str()] = line.str(); + continue; + } + const bool definition = line.starts_with("define "); + if (definition || line.starts_with("declare ")) + { + const std::size_t at = line.find('@'); + const std::string name = symbolAt(line.str(), at + 1).first; + if (definition) + { + current = name; + body = line.str() + "\n"; + out.defined.insert(name); + } + else + { + out.globals[name] = line.str(); + } + continue; + } + if (line.starts_with("@")) + { + const std::string name = symbolAt(line.str(), 1).first; + out.globals[name] = line.str(); + if (!line.contains("= external ") && !line.contains("= extern_weak ") && + !line.contains("= dllimport external ")) + { + out.defined.insert(name); + } + continue; + } + // Module-level assembly. + out.globals[line.str()] = line.str(); + } + return out; + } + + // Rewrites `text` symbol by symbol and attribute group by contents. Each metadata it uses + // is numbered by first use and its definition appended, so metadata are compared by + // contents and by the places that use them, not by their numbers. Quoted strings are left + // alone. + std::string normalized(const std::string& text, + const std::map& rename, + const std::map& attributeGroups, + const std::map& metadata) + { + std::string out; + std::map numbers; + std::vector used; + const auto scan = [&](const std::string& piece) + { + bool quoted = false; + for (std::size_t i = 0; i < piece.size();) + { + const char c = piece[i]; + if (c == '"' && !(i > 0 && piece[i - 1] == '@')) + { + quoted = !quoted; + out += c; + ++i; + continue; + } + if (quoted) + { + out += c; + ++i; + continue; + } + if (c == '@' || c == '$') + { + const auto [name, length] = symbolAt(piece, i + 1); + if (length != 0) + { + const auto renamed = rename.find(name); + out += c; + out += spellSymbol(renamed == rename.end() ? name : renamed->second); + i += 1 + length; + continue; + } + } + const bool numbered = i + 1 < piece.size() && + std::isdigit(static_cast(piece[i + 1])); + if ((c == '!' || c == '#') && numbered) + { + std::size_t end = i + 1; + while (end < piece.size() && + std::isdigit(static_cast(piece[end]))) + ++end; + const std::string reference = piece.substr(i, end - i); + if (c == '!') + { + const auto [it, first] = numbers.try_emplace(reference, numbers.size()); + if (first) + used.push_back(reference); + out += "!m" + std::to_string(it->second); + } + else + { + const auto group = attributeGroups.find(reference); + out += group == attributeGroups.end() ? reference : "#" + group->second; + } + i = end; + continue; + } + out += c; + ++i; + } + }; + scan(text); + for (std::size_t i = 0; i < used.size(); ++i) + { + out += "\n!m" + std::to_string(i) + " = "; + const auto definition = metadata.find(used[i]); + scan(definition == metadata.end() ? "?" : definition->second); + } + return out; + } + + // The contents of a string constant defined on `line`, and where they start and end. + bool stringConstant(const std::string& line, std::size_t& begin, std::size_t& end) + { + const std::size_t marker = line.find(" c\""); + if (marker == std::string::npos || line.find(" constant [") == std::string::npos) + return false; + begin = marker + 3; + end = line.find("\\00\"", begin); + return end != std::string::npos; + } + + std::string escaped(llvm::StringRef text) + { + std::string out; + llvm::raw_string_ostream os(out); + llvm::printEscapedString(text, os); + os.flush(); + return out; + } + + // `line` with the contents and length of its string constant replaced. + std::string withString(const std::string& line, std::size_t begin, std::size_t end, + const std::string& contents, std::size_t length) + { + std::string out = line.substr(0, begin) + contents + line.substr(end); + const std::size_t array = out.find(" constant ["); + const std::size_t times = out.find(" x i8]", array); + if (array != std::string::npos && times != std::string::npos) + out.replace(array + 11, times - array - 11, std::to_string(length + 1)); + return out; + } + + std::size_t unescapedLength(llvm::StringRef escapedText) + { + std::size_t length = 0; + for (std::size_t i = 0; i < escapedText.size(); ++i, ++length) + { + if (escapedText[i] == '\\') + i += 2; + } + return length; + } + + std::string firstDifference(const std::string& before, const std::string& after) + { + const auto a = llvm::to_vector(llvm::split(before, '\n')); + const auto b = llvm::to_vector(llvm::split(after, '\n')); + std::size_t i = 0; + while (i < a.size() && i < b.size() && a[i] == b[i]) + ++i; + const std::string was = i < a.size() ? a[i].trim().str() : "(end)"; + const std::string now = i < b.size() ? b[i].trim().str() : "(end)"; + return "'" + was.substr(0, 160) + "' became '" + now.substr(0, 160) + "'"; + } + } // namespace + + const_suggestion::ModuleText const_suggestion::prepare(llvm::Module& module) + { + return textOf(module); + } + + std::optional rewrittenCodeDifference(llvm::Module& before, llvm::Module& after, + const std::vector& established, + const PrintedSignature* signature, + std::vector* accepted) + { + return const_suggestion::codeDifference(textOf(before), textOf(after), established, + signature, accepted); + } + + std::optional const_suggestion::codeDifference( + const ModuleText& was, const ModuleText& now, const std::vector& established, + const PrintedSignature* signature, std::vector* accepted) + { + std::vector removed; + std::vector added; + for (const auto& [name, text] : was.globals) + { + if (!now.globals.count(name)) + removed.push_back(name); + } + for (const auto& [name, text] : now.globals) + { + if (!was.globals.count(name)) + added.push_back(name); + } + + // The symbols the rewrite renames, by the pairs its correspondences established and by + // them only (§5.9): a symbol that only one side has and no pair covers is a difference. + std::map rename; + std::set taken; + for (const SymbolPair& pair : established) + { + if (std::binary_search(removed.begin(), removed.end(), pair.before) && + std::binary_search(added.begin(), added.end(), pair.after)) + { + rename[pair.before] = pair.after; + taken.insert(pair.after); + } + } + std::vector unpaired; + for (const std::string& name : removed) + { + if (!rename.count(name)) + unpaired.push_back(name); + } + std::vector unexpected; + for (const std::string& name : added) + { + if (!taken.count(name)) + unexpected.push_back(name); + } + if (!unpaired.empty() || !unexpected.empty()) + { + const auto list = [](const std::vector& names) + { return names.empty() ? std::string("none") : llvm::join(names, ", "); }; + return "symbols no longer used: " + list(unpaired) + + "; symbols used only after the rewrite: " + list(unexpected); + } + + if (was.types != now.types) + { + for (const auto& [name, definition] : was.types) + { + const auto other = now.types.find(name); + if (other == now.types.end() || other->second != definition) + return "type " + name + " changed"; + } + return "types added by the rewrite"; + } + + bool signatureAccepted = false; + for (const auto& [name, text] : was.globals) + { + const auto renamed = rename.find(name); + const std::string newName = renamed == rename.end() ? name : renamed->second; + const std::string expected = + normalized(text, rename, was.attributeGroups, was.metadata); + std::string actual = + normalized(now.globals.at(newName), {}, now.attributeGroups, now.metadata); + if (expected == actual) + continue; + + std::size_t begin = 0; + std::size_t end = 0; + std::size_t newBegin = 0; + std::size_t newEnd = 0; + if (stringConstant(expected, begin, end) && stringConstant(actual, newBegin, newEnd)) + { + const std::string oldText = expected.substr(begin, end - begin); + const std::string newText = actual.substr(newBegin, newEnd - newBegin); + // A type name holds the mangled name of its type, renamed with the function. + const std::string prefix = oldText.starts_with("*") ? "*" : ""; + const bool typeName = name.starts_with("_ZTS") && renamed != rename.end() && + oldText == prefix + name.substr(4) && + newText == prefix + newName.substr(4); + // A string whose only change is the function's printed signature. + bool printedSignature = false; + if (!typeName && signature && signature->before != signature->after) + { + const std::string from = escaped(signature->before); + const std::string to = escaped(signature->after); + std::string replaced = oldText; + for (std::size_t at = from.empty() ? std::string::npos : replaced.find(from); + at != std::string::npos; at = replaced.find(from, at + to.size())) + { + replaced.replace(at, from.size(), to); + printedSignature = true; + } + printedSignature = printedSignature && replaced == newText; + } + if (typeName || printedSignature) + { + actual = + withString(actual, newBegin, newEnd, oldText, unescapedLength(oldText)); + if (actual == expected) + { + if (accepted && typeName) + { + accepted->push_back("difference: the name typeid gives a type named " + "after the function changes, from \"" + + oldText + "\" to \"" + newText + "\""); + } + if (accepted && printedSignature && !signatureAccepted) + { + accepted->push_back("difference: the signature printed by " + "__PRETTY_FUNCTION__ changes, from \"" + + signature->before + "\" to \"" + signature->after + + "\""); + } + signatureAccepted = signatureAccepted || printedSignature; + continue; + } + } + } + return "code changed in " + name + ": " + firstDifference(expected, actual); + } + for (const auto& [name, operands] : was.namedMetadata) + { + const auto other = now.namedMetadata.find(name); + const std::string expected = + normalized(operands, rename, was.attributeGroups, was.metadata); + const std::string actual = + other == now.namedMetadata.end() + ? "(none)" + : normalized(other->second, {}, now.attributeGroups, now.metadata); + if (expected != actual) + return "metadata " + name + " changed: " + firstDifference(expected, actual); + } + for (const auto& [name, operands] : now.namedMetadata) + { + if (!was.namedMetadata.count(name)) + return "metadata " + name + " only after the rewrite"; + } + return std::nullopt; + } +} // namespace ctrace::stack::analysis diff --git a/src/analysis/ConstSuggestionFacts.cpp b/src/analysis/ConstSuggestionFacts.cpp new file mode 100644 index 0000000..1e930b0 --- /dev/null +++ b/src/analysis/ConstSuggestionFacts.cpp @@ -0,0 +1,1678 @@ +// SPDX-License-Identifier: Apache-2.0 +// Compiles one unit for the Clang check of const suggestions (#217) and inspects it: its +// diagnostics, its generated code, and what it shows of the functions the suggestions name. +#include "ConstSuggestionInternal.hpp" +#include "analysis/InputPipeline.hpp" +#include "analyzer/HotspotProfiler.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ctrace::stack::analysis +{ + namespace + { + using const_suggestion::candidateOf; + using const_suggestion::DeclarationSite; + using const_suggestion::Edit; + using const_suggestion::EntityIdentity; + using const_suggestion::Fact; + using const_suggestion::FactKind; + using const_suggestion::linkageOf; + using const_suggestion::realPath; + using const_suggestion::SourceLinkage; + using const_suggestion::SourcePosition; + using const_suggestion::SuggestionPlan; + using const_suggestion::Target; + using const_suggestion::UnitFacts; + using const_suggestion::writtenType; + + // Warnings that the C and C++ standards make constraint violations, or that a rewrite + // turns on silently: the rewritten code must not add any. + constexpr const char* kCheckedWarnings[] = { + "incompatible-pointer-types-discards-qualifiers", + "incompatible-pointer-types", + "incompatible-function-pointer-types", + "int-conversion", + "overloaded-virtual", + "pointer-type-mismatch"}; + + // Errors, and the checked warnings, by place and kind: their texts name types that the + // rewrite changes. + class DiagnosticCollector : public clang::DiagnosticConsumer + { + public: + explicit DiagnosticCollector(UnitFacts& out) : out_(out) {} + + void HandleDiagnostic(clang::DiagnosticsEngine::Level level, + const clang::Diagnostic& info) override + { + clang::DiagnosticConsumer::HandleDiagnostic(level, info); + bool kept = level >= clang::DiagnosticsEngine::Error; + if (level == clang::DiagnosticsEngine::Warning) + { + const llvm::StringRef group = + info.getDiags()->getDiagnosticIDs()->getWarningOptionForDiag(info.getID()); + kept = std::find(std::begin(kCheckedWarnings), std::end(kCheckedWarnings), + group) != std::end(kCheckedWarnings); + } + if (!kept) + return; + std::string place = "?"; + if (info.hasSourceManager() && info.getLocation().isValid()) + { + const clang::SourceManager& sm = info.getSourceManager(); + const clang::PresumedLoc presumed = + sm.getPresumedLoc(sm.getExpansionLoc(info.getLocation())); + if (presumed.isValid()) + { + place = realPath(presumed.getFilename()) + ":" + + std::to_string(presumed.getLine()); + } + } + const std::string key = place + " #" + std::to_string(info.getID()); + out_.diagnostics.insert(key); + llvm::SmallString<160> text; + info.FormatDiagnostic(text); + out_.reported.push_back({key, place + ": " + text.str().str(), + level >= clang::DiagnosticsEngine::Error}); + if (out_.firstDiagnostic.empty()) + out_.firstDiagnostic = out_.reported.back().text; + if (level == clang::DiagnosticsEngine::Fatal && out_.fatal.empty()) + out_.fatal = out_.reported.back().text; + } + + private: + UnitFacts& out_; + }; + + // An offset of the rewritten text, in the original one. An offset inside inserted text + // maps to the edit's position. + std::size_t originalOffset(const std::vector& sortedEdits, std::size_t offset) + { + std::size_t inserted = 0; // text the edits before `offset` add + std::size_t removed = 0; // and remove + for (const Edit& edit : sortedEdits) + { + const std::size_t at = edit.offset + inserted - removed; + if (offset < at) + break; + if (offset < at + edit.text.size()) + return edit.offset; + inserted += edit.text.size(); + removed += edit.length; + } + return offset - inserted + removed; + } + + // The symbol CodeGen gives each variant of a function: a constructor's complete and base + // ones, a destructor's deleting, complete and base ones, otherwise the function's. It + // numbers unnamed types, such as a lambda ($_0, $_1), in the order it mangles them, as the + // IR names them. + std::vector variantSymbols(clang::CodeGenerator& generator, + const clang::FunctionDecl& function) + { + if (function.isDependentContext()) + return {}; + const auto symbol = [&](clang::GlobalDecl variant) + { return generator.GetMangledName(variant).str(); }; + if (const auto* constructor = llvm::dyn_cast(&function)) + { + return {symbol({constructor, clang::Ctor_Complete}), + symbol({constructor, clang::Ctor_Base})}; + } + if (const auto* destructor = llvm::dyn_cast(&function)) + { + return {symbol({destructor, clang::Dtor_Deleting}), + symbol({destructor, clang::Dtor_Complete}), + symbol({destructor, clang::Dtor_Base})}; + } + return {symbol(clang::GlobalDecl(&function))}; + } + + // The variant each of variantSymbols names, in the same order. + std::vector variantKeys(const clang::FunctionDecl& function) + { + if (function.isDependentContext()) + return {}; + if (llvm::isa(function)) + return {"C1", "C2"}; + if (llvm::isa(function)) + return {"D0", "D1", "D2"}; + return {""}; + } + + // A specialization's template arguments, its class's then its own, each type written + // canonically (ruling 7); "" for a function that is no specialization. + std::string argumentsOf(const clang::ASTContext& context, + const clang::FunctionDecl& function) + { + clang::PrintingPolicy policy(context.getLangOpts()); + policy.AnonymousTagLocations = false; + std::string out; + const auto add = [&](llvm::ArrayRef arguments) + { + out += "<"; + for (const clang::TemplateArgument& argument : arguments) + { + if (argument.getKind() == clang::TemplateArgument::Type) + { + out += writtenType(context, argument.getAsType()); + } + else + { + llvm::raw_string_ostream text(out); + argument.print(policy, text, /*IncludeType=*/true); + } + out += ","; + } + out += ">"; + }; + if (const auto* record = llvm::dyn_cast( + function.getDeclContext())) + { + add(record->getTemplateArgs().asArray()); + } + if (const clang::TemplateArgumentList* arguments = + function.getTemplateSpecializationArgs()) + { + add(arguments->asArray()); + } + return out; + } + + // Whether a declaration comes from a template instantiation: a specialization the + // compilation instantiates, or a declaration inside one. + bool instantiated(const clang::Decl* declaration) + { + while (declaration) + { + const auto* function = llvm::dyn_cast(declaration); + const auto* variable = llvm::dyn_cast(declaration); + const auto* record = llvm::dyn_cast(declaration); + if ((function && function->isTemplateInstantiation()) || + (variable && + clang::isTemplateInstantiation(variable->getTemplateSpecializationKind())) || + (record && + clang::isTemplateInstantiation(record->getTemplateSpecializationKind()))) + { + return true; + } + const clang::DeclContext* context = declaration->getDeclContext(); + declaration = context ? clang::Decl::castFromDeclContext(context) : nullptr; + } + return false; + } + + // A site, as an R or O value holds it. + std::string textOf(const DeclarationSite& site) + { + return site.expansion.file + ":" + std::to_string(site.expansion.offset) + "|" + + site.spelling.file + ":" + std::to_string(site.spelling.offset); + } + + // Each target's function, found by its declarations (§3, §5.5), the explicit casts of its + // address, and the facts R, O and K (§5.4). + // §5.6 (D19): a type as the source writes it. A deduced part (auto, decltype(auto), + // decltype, typeof, a deduced class template's arguments) is written as its keyword, the + // rest canonically, so that what deduction gives it, a consequence of the plan, is left + // to steps 4 to 7. `deduced` tells whether the type has such a part. + std::string writtenForm(const clang::ASTContext& context, clang::QualType type, + bool& deduced) + { + const clang::PrintingPolicy policy(context.getLangOpts()); + const std::string qualifiers = type.getLocalQualifiers().getAsString(policy); + const auto qualified = [&](const std::string& text) + { return qualifiers.empty() ? text : qualifiers + " " + text; }; + const auto part = [&](clang::QualType inner) + { return writtenForm(context, inner, deduced); }; + const clang::Type* node = type.getTypePtr(); + if (const auto* deducedTemplate = + llvm::dyn_cast(node)) + { + deduced = true; + std::string name; + llvm::raw_string_ostream out(name); + deducedTemplate->getTemplateName().print(out, policy); + return qualified(name); + } + if (const auto* automatic = llvm::dyn_cast(node)) + { + deduced = true; + return qualified(automatic->isDecltypeAuto() ? "decltype(auto)" : "auto"); + } + if (llvm::isa(node)) + { + deduced = true; + return qualified("decltype"); + } + if (const auto* pointer = llvm::dyn_cast(node)) + return qualified(part(pointer->getPointeeType()) + " *"); + if (const auto* reference = llvm::dyn_cast(node)) + { + return qualified(part(reference->getPointeeTypeAsWritten()) + + (reference->isLValueReferenceType() ? " &" : " &&")); + } + if (const auto* member = llvm::dyn_cast(node)) + { + return qualified(part(member->getPointeeType()) + " " + + writtenType(context, clang::QualType(member->getClass(), 0)) + + "::*"); + } + if (const auto* array = llvm::dyn_cast(node)) + { + const auto* constant = llvm::dyn_cast(array); + return qualified(part(array->getElementType()) + " [" + + (constant ? llvm::toString(constant->getSize(), 10, false) : "") + + "]"); + } + if (const auto* function = llvm::dyn_cast(node)) + { + std::string out = part(function->getReturnType()) + " ("; + for (unsigned i = 0; i < function->getNumParams(); ++i) + out += (i ? ", " : "") + part(function->getParamType(i)); + out += function->isVariadic() ? ", ...)" : ")"; + const std::string method = function->getMethodQuals().getAsString(policy); + return qualified(method.empty() ? out : out + " " + method); + } + if (const auto* specialization = + llvm::dyn_cast(node); + specialization && !specialization->isTypeAlias()) + { + std::string out; + llvm::raw_string_ostream text(out); + specialization->getTemplateName().print(text, policy); + text << "<"; + bool first = true; + for (const clang::TemplateArgument& argument : specialization->template_arguments()) + { + text << (first ? "" : ", "); + first = false; + if (argument.getKind() == clang::TemplateArgument::Type) + text << part(argument.getAsType()); + else + argument.print(policy, text, /*IncludeType=*/true); + } + text << ">"; + return qualified(out); + } + if (llvm::isa(node)) + { + return qualified(part(node->getLocallyUnqualifiedSingleStepDesugaredType())); + } + return qualified(writtenType(context, clang::QualType(node, 0))); + } + + class Inspector : public clang::ASTConsumer, public clang::RecursiveASTVisitor + { + public: + Inspector(clang::CodeGenAction& action, const std::vector& targets, + const SuggestionPlan* rewrittenBy, UnitFacts& out) + : action_(action), targets_(targets), rewrittenBy_(rewrittenBy), out_(out) + { + } + + bool shouldVisitTemplateInstantiations() const + { + return true; + } + + // The instantiations around a fact, by their symbols. + bool TraverseDecl(clang::Decl* declaration) + { + const auto* function = llvm::dyn_cast_or_null(declaration); + const bool instantiation = collectFacts_ && function && + function->isTemplateInstantiation() && + !symbolsOf(*function).empty(); + if (instantiation) + { + contexts_.push_back(symbolsOf(*function).front()); + out_.instantiations.insert(contexts_.back()); + } + const bool traversed = + clang::RecursiveASTVisitor::TraverseDecl(declaration); + if (instantiation) + contexts_.pop_back(); + return traversed; + } + + void HandleTranslationUnit(clang::ASTContext& context) override + { + context_ = &context; + generator_ = action_.getCodeGenerator(); + mangle_.reset(context.createMangleContext()); + // After an error, no fact is needed: the rewrite is refuted by the error it adds + // (§5.8, step 4), or kept as not checked when it adds none. + collectFacts_ = generator_ && !context.getDiagnostics().hasErrorOccurred(); + sites_.emplace(context.getSourceManager(), rewrittenBy_); + out_.file = sites_->fileOf(context.getSourceManager().getMainFileID()); + out_.entities.resize(targets_.size()); + located_.assign(targets_.size(), nullptr); + atSites_.assign(targets_.size(), {}); + linked_.assign(targets_.size(), {}); + for (std::size_t i = 0; i < targets_.size(); ++i) + { + if (!targets_[i].plan) + planAt(i); + } + // The files of the planned declarations: no other file has their sites. + for (std::size_t i = 0; i < targets_.size(); ++i) + { + for (const DeclarationSite& site : identity(i).redeclarations) + declarationFiles_.insert(site.expansion.file); + } + // The files that a plan rewrites: after the rewrite, those of its plan. + for (std::size_t i = 0; i < targets_.size(); ++i) + { + const SuggestionPlan& plan = + targets_[i].plan ? *targets_[i].plan : out_.entities[i].plan; + if (rewrittenBy_ && &plan != rewrittenBy_) + continue; + for (const auto& [path, edits] : plan.edits) + rewrittenFiles_.insert(path); + } + const analyzer::ScopedHotspot hotspot(out_.timing, "const_suggestion.inspect"); + TraverseDecl(context.getTranslationUnitDecl()); + for (std::size_t i = 0; i < targets_.size(); ++i) + resolve(i); + { + const analyzer::ScopedHotspot identifying(out_.timing, + "const_suggestion.identify"); + identify(); + } + } + + bool VisitFunctionDecl(clang::FunctionDecl* function) + { + const clang::SourceManager& sm = context_->getSourceManager(); + const clang::SourceLocation location = function->getLocation(); + std::optional site; + if (location.isValid() && declarationFiles_.count(sites_->fileOf( + sm.getFileID(sm.getExpansionLoc(location))))) + { + site = (*sites_)(location); + } + const std::string& name = nameOf(function->getDeclName()); + for (std::size_t i = 0; i < targets_.size(); ++i) + { + const EntityIdentity& entity = identity(i); + if (targets_[i].plan) + { + if (site && std::binary_search(entity.redeclarations.begin(), + entity.redeclarations.end(), *site)) + { + atSites_[i].push_back(function); + } + else if (entity.linkage == SourceLinkage::External && name == entity.name && + linkageOf(*function) == SourceLinkage::External) + { + linked_[i].push_back(function); + } + } + const auto* method = llvm::dyn_cast(function); + if (collectFacts_ && method && method->isFirstDecl() && name == entity.name) + out_.entities[i].overrides.push_back(overridesOf(*method)); + } + return true; + } + + bool VisitExplicitCastExpr(clang::ExplicitCastExpr* cast) + { + // `(void)f` converts the function to no other type. + if (cast->getType()->isVoidType()) + return true; + const clang::Expr* operand = cast->getSubExpr()->IgnoreParenImpCasts(); + if (const auto* address = llvm::dyn_cast(operand)) + { + if (address->getOpcode() == clang::UO_AddrOf) + operand = address->getSubExpr()->IgnoreParenImpCasts(); + } + const auto* reference = llvm::dyn_cast(operand); + const auto* function = + reference ? llvm::dyn_cast(reference->getDecl()) : nullptr; + if (function && cast->getBeginLoc().isValid()) + casts_.push_back({function->getCanonicalDecl(), cast->getBeginLoc()}); + return true; + } + + // Nested symbols (§5.7), which the rewrite renames with the function around them: a + // lambda's call operator, a local class's methods and type information, a static + // local variable and its guard. + bool VisitLambdaExpr(clang::LambdaExpr* lambda) + { + const clang::CXXMethodDecl* call = lambda->getCallOperator(); + if (!collectFacts_ || !call || call->isDependentContext()) + return true; + const std::vector& symbols = symbolsOf(*call); + if (!symbols.empty()) + { + nested(lambda->getLambdaClass()->getDeclContext(), lambda->getBeginLoc(), + {{symbols.front(), "lambda"}}); + } + return true; + } + + bool VisitCXXRecordDecl(clang::CXXRecordDecl* record) + { + if (!collectFacts_ || record->isDependentType() || + !record->isThisDeclarationADefinition()) + { + return true; + } + inheritingIn(*record); + if (!record->isLocalClass() || record->isLambda()) + return true; + const clang::QualType type = context_->getRecordType(record); + std::string information; + std::string name; + { + llvm::raw_string_ostream out(information); + mangle_->mangleCXXRTTI(type, out); + } + { + llvm::raw_string_ostream out(name); + mangle_->mangleCXXRTTIName(type, out); + } + std::vector> symbols = { + {information, "type information"}, {name, "type name"}}; + auto* itanium = llvm::dyn_cast(mangle_.get()); + if (itanium && record->isDynamicClass()) + { + std::string table; + llvm::raw_string_ostream out(table); + itanium->mangleCXXVTable(record, out); + symbols.push_back({table, "virtual table"}); + } + nested(record->getDeclContext(), record->getLocation(), symbols); + for (const clang::CXXMethodDecl* method : record->methods()) + { + const std::vector& variants = symbolsOf(*method); + const std::vector keys = variantKeys(*method); + std::vector> methods; + for (std::size_t k = 0; k < variants.size() && k < keys.size(); ++k) + methods.push_back({variants[k], "method " + keys[k]}); + nested(record->getDeclContext(), method->getLocation(), methods); + } + return true; + } + + // §5.6 (D19), after the rewrite: the planned parameters' types are the plan's. A type + // that an edit writes anywhere else, in a declaration, an expression or a planned + // parameter's default argument, is outside it. + bool TraverseParmVarDecl(clang::ParmVarDecl* parameter) + { + const clang::TypeSourceInfo* outer = plannedType_; + if (rewrittenBy_ && parameter->getTypeSourceInfo() && + std::binary_search(rewrittenBy_->parameters.begin(), + rewrittenBy_->parameters.end(), + (*sites_)(parameter->getLocation()))) + { + plannedType_ = parameter->getTypeSourceInfo(); + } + const bool traversed = + clang::RecursiveASTVisitor::TraverseParmVarDecl(parameter); + plannedType_ = outer; + return traversed; + } + + bool TraverseTypeLoc(clang::TypeLoc loc) + { + const bool planned = plannedType_ && loc == plannedType_->getTypeLoc(); + inPlannedType_ += planned; + const bool traversed = clang::RecursiveASTVisitor::TraverseTypeLoc(loc); + inPlannedType_ -= planned; + return traversed; + } + + bool VisitTypeLoc(clang::TypeLoc loc) + { + if (!rewrittenBy_ || inPlannedType_ || out_.writtenOutside) + return true; + const clang::SourceLocation at = loc.getBeginLoc(); + if (at.isInvalid()) + return true; + const clang::SourceManager& sm = context_->getSourceManager(); + const clang::FileID file = sm.getFileID(sm.getSpellingLoc(at)); + const auto known = rewrittenSpelling_.find(file.getHashValue()); + const bool rewritten = known != rewrittenSpelling_.end() + ? known->second + : (rewrittenSpelling_[file.getHashValue()] = + rewrittenFiles_.count(sites_->fileOf(file)) != 0); + if (!rewritten) + return true; + const DeclarationSite site = (*sites_)(at); + if (sites_->edited(site.spelling)) + out_.writtenOutside = UnitFacts::Outside{site, lineOf(at)}; + return true; + } + + bool VisitVarDecl(clang::VarDecl* variable) + { + if (!collectFacts_ || !variable->isStaticLocal() || + variable->getDeclContext()->isDependentContext()) + { + return true; + } + std::string guard; + { + llvm::raw_string_ostream out(guard); + mangle_->mangleStaticGuardVariable(variable, out); + } + nested(variable->getDeclContext(), variable->getLocation(), + {{generator_->GetMangledName(clang::GlobalDecl(variable)).str(), "static"}, + {guard, "guard"}}); + return true; + } + + // §5.6: the type of each declaration that a rewritten file writes, by its site. + bool VisitDeclaratorDecl(const clang::DeclaratorDecl* declaration) + { + written(*declaration, declaration->getType()); + return true; + } + + bool VisitTypedefNameDecl(const clang::TypedefNameDecl* declaration) + { + written(*declaration, declaration->getUnderlyingType()); + return true; + } + + // R: what a call or a reference targets, unevaluated operands and instantiations + // included. + bool VisitDeclRefExpr(clang::DeclRefExpr* expression) + { + reference(expression->getDecl(), expression->getLocation()); + return true; + } + + bool VisitMemberExpr(const clang::MemberExpr* expression) + { + reference(expression->getMemberDecl(), expression->getMemberLoc()); + return true; + } + + bool VisitCXXConstructExpr(const clang::CXXConstructExpr* expression) + { + reference(expression->getConstructor(), expression->getLocation()); + return true; + } + + bool VisitCXXNewExpr(const clang::CXXNewExpr* expression) + { + reference(expression->getOperatorNew(), expression->getBeginLoc()); + return true; + } + + bool VisitCXXDeleteExpr(const clang::CXXDeleteExpr* expression) + { + reference(expression->getOperatorDelete(), expression->getBeginLoc()); + return true; + } + + // K: each if constexpr condition, and the association each _Generic selects. + bool VisitIfStmt(clang::IfStmt* statement) + { + const clang::Expr* condition = statement->getCond(); + bool value = false; + if (collectFacts_ && statement->isConstexpr() && condition && + !condition->isValueDependent() && + condition->EvaluateAsBooleanCondition(value, *context_)) + { + constant(statement->getIfLoc(), value ? "true" : "false", ""); + } + return true; + } + + bool VisitGenericSelectionExpr(const clang::GenericSelectionExpr* selection) + { + if (collectFacts_ && !selection->isResultDependent()) + { + constant(selection->getGenericLoc(), + std::to_string(selection->getResultIndex()), "_Generic"); + } + return true; + } + + private: + // A symbol, cached by declaration. + const std::vector& symbolsOf(const clang::FunctionDecl& function) + { + const clang::FunctionDecl* canonical = function.getCanonicalDecl(); + auto [it, added] = symbols_.try_emplace(canonical); + if (added) + it->second = variantSymbols(*generator_, *canonical); + return it->second; + } + + // A function, as a reason names it. + const std::string& shownOf(const clang::FunctionDecl& function) + { + const clang::FunctionDecl* canonical = function.getCanonicalDecl(); + auto [it, added] = shown_.try_emplace(canonical); + if (added) + { + const std::vector& symbols = symbolsOf(*canonical); + it->second = symbols.empty() ? canonical->getQualifiedNameAsString() + : llvm::demangle(symbols.front()); + } + return it->second; + } + + // An R value (ruling 4): the target's declaration sites; a specialization or an + // implicit function, which has none of its own, by its symbol. + const std::vector& valueOf(const clang::FunctionDecl& target) + { + const clang::FunctionDecl* canonical = target.getCanonicalDecl(); + auto [it, added] = values_.try_emplace(canonical); + std::vector& value = it->second; + if (!added) + return value; + if (canonical->isImplicit() || canonical->isTemplateInstantiation()) + { + const std::vector& symbols = symbolsOf(*canonical); + if (symbols.empty()) + return value; + value.push_back("symbol:" + symbols.front()); + // A specialization also names its template: two specializations of one + // template may correspond (§5.7). + if (const clang::FunctionDecl* pattern = + canonical->getTemplateInstantiationPattern(/*ForDefinition=*/false)) + { + value.push_back("template:" + + textOf((*sites_)(pattern->getFirstDecl()->getLocation()))); + } + return value; + } + for (const clang::FunctionDecl* declaration : canonical->redecls()) + value.push_back("site:" + textOf((*sites_)(declaration->getLocation()))); + std::sort(value.begin(), value.end()); + value.erase(std::unique(value.begin(), value.end()), value.end()); + return value; + } + + // Neither implicit nor instantiated, valid or not: a declaration that the rewrite + // makes invalid outside the plan is still one that it changes. + void written(const clang::NamedDecl& declaration, clang::QualType type) + { + const clang::SourceLocation at = declaration.getLocation(); + if (rewrittenFiles_.empty() || at.isInvalid() || declaration.isImplicit() || + instantiated(&declaration)) + { + return; + } + const clang::SourceManager& sm = context_->getSourceManager(); + if (!rewrittenFiles_.count(sites_->fileOf(sm.getFileID(sm.getExpansionLoc(at))))) + return; + std::string name = declaration.getNameAsString(); + bool deduced = false; + std::string form = writtenForm(*context_, type, deduced); + out_.writtenTypes[(*sites_)(at)].push_back(UnitFacts::Written{ + deduced ? std::move(form) : writtenType(*context_, type), + name.empty() ? "an unnamed declaration" : std::move(name), lineOf(at)}); + } + + unsigned lineOf(clang::SourceLocation at) const + { + return context_->getSourceManager().getExpansionLineNumber(at); + } + + void reference(const clang::ValueDecl* declaration, clang::SourceLocation at) + { + const auto* target = llvm::dyn_cast_or_null(declaration); + if (!collectFacts_ || !target || at.isInvalid()) + return; + const std::vector& value = valueOf(*target); + if (value.empty()) + return; + out_.facts.push_back({{contexts_, (*sites_)(at)}, + value, + "", + shownOf(*target), + lineOf(at), + FactKind::Reference}); + } + + void constant(clang::SourceLocation at, std::string value, std::string subject) + { + out_.facts.push_back({{contexts_, (*sites_)(at)}, + {std::move(value)}, + std::move(subject), + "", + lineOf(at), + FactKind::Constant}); + } + + // O: the methods that a method of the entity's name overrides. + Fact overridesOf(const clang::CXXMethodDecl& method) + { + Fact out{{contexts_, (*sites_)(method.getLocation())}, + {}, + shownOf(method), + "", + lineOf(method.getLocation()), + FactKind::Override}; + std::vector shown; + for (const clang::CXXMethodDecl* base : method.overridden_methods()) + { + out.value.push_back(llvm::join(valueOf(*base), ";")); + shown.push_back("'" + shownOf(*base) + "'"); + } + std::sort(out.value.begin(), out.value.end()); + std::sort(shown.begin(), shown.end()); + out.shown = shown.empty() ? "nothing" : llvm::join(shown, ", "); + return out; + } + + const std::string& nameOf(clang::DeclarationName name) + { + auto [it, added] = names_.try_emplace(name.getAsOpaquePtr()); + if (added) + it->second = name.getAsString(); + return it->second; + } + + // Whether a function has one of the sites of the entity of target i: it is one of + // its declarations, or a specialization of its template. + bool atSites(std::size_t i, const clang::FunctionDecl& function) + { + const clang::SourceLocation location = function.getLocation(); + if (location.isInvalid()) + return false; + const clang::SourceManager& sm = context_->getSourceManager(); + if (!declarationFiles_.count( + sites_->fileOf(sm.getFileID(sm.getExpansionLoc(location))))) + { + return false; + } + const EntityIdentity& entity = identity(i); + return std::binary_search(entity.redeclarations.begin(), + entity.redeclarations.end(), (*sites_)(location)); + } + + // The symbol of the function around a nested declaration, when its outermost + // function, through lambdas and local classes, is the entity of target i or a + // specialization of its template; "" otherwise. + std::string enclosingSymbol(std::size_t i, const clang::DeclContext* context) + { + const clang::FunctionDecl* immediate = nullptr; + for (const clang::DeclContext* at = context; at; at = at->getParent()) + { + const auto* function = llvm::dyn_cast(at); + if (!function) + continue; + if (!immediate) + immediate = function; + const auto* method = llvm::dyn_cast(function); + if (method && + (method->getParent()->isLambda() || method->getParent()->isLocalClass())) + { + continue; + } + if (function->isDependentContext() || !atSites(i, *function)) + return ""; + const std::vector& symbols = symbolsOf(*immediate); + return symbols.empty() ? "" : symbols.front(); + } + return ""; + } + + // The symbol of the function around a nested type (§5.7, step 5); "" in a template. + std::string enclosingOf(const clang::CXXRecordDecl& record) + { + for (const clang::DeclContext* at = record.getDeclContext(); at; + at = at->getParent()) + { + if (const auto* function = llvm::dyn_cast(at)) + { + if (function->isDependentContext()) + return ""; + const std::vector& symbols = symbolsOf(*function); + return symbols.empty() ? "" : symbols.front(); + } + } + return ""; + } + + // A template argument's type, canonically (§5.7, step 5), each nested type written as + // its record's site and the symbol of the function around it, which the + // correspondences map; every other part, qualifiers included, kept as it is. + std::string typeKey(clang::QualType type, bool& nested) + { + type = type.getCanonicalType(); + const std::string qualifiers = type.getLocalQualifiers().getAsString(); + const auto with = [&](const std::string& text) + { return qualifiers.empty() ? text : qualifiers + " " + text; }; + const clang::Type* node = type.getTypePtr(); + if (const auto* pointer = llvm::dyn_cast(node)) + return with(typeKey(pointer->getPointeeType(), nested) + " *"); + if (const auto* reference = llvm::dyn_cast(node)) + { + return with(typeKey(reference->getPointeeType(), nested) + + (reference->isLValueReferenceType() ? " &" : " &&")); + } + if (const auto* array = llvm::dyn_cast(node)) + { + return with(typeKey(array->getElementType(), nested) + " [" + + llvm::toString(array->getSize(), 10, false) + "]"); + } + if (const auto* function = llvm::dyn_cast(node)) + { + std::string out = typeKey(function->getReturnType(), nested) + " ("; + for (unsigned i = 0; i < function->getNumParams(); ++i) + out += (i ? ", " : "") + typeKey(function->getParamType(i), nested); + return with(out + (function->isVariadic() ? ", ...)" : ")")); + } + if (const clang::CXXRecordDecl* record = node->getAsCXXRecordDecl()) + { + if (record->isLambda() || record->isLocalClass()) + { + nested = true; + return with("\x01" + textOf((*sites_)(record->getLocation())) + "\x02" + + enclosingOf(*record) + "\x03"); + } + // A member class is written through the class around it, whose arguments + // may hold a nested type: std::vector::__destroy_vector. + const auto* parent = + llvm::dyn_cast(record->getDeclContext()); + const std::string outer = + parent ? typeKey(context_->getRecordType(parent), nested) + "::" : ""; + if (const auto* specialization = + llvm::dyn_cast(record)) + { + return with( + (parent ? outer + specialization->getNameAsString() + : specialization->getSpecializedTemplate() + ->getQualifiedNameAsString()) + + "<" + + argumentsKey(specialization->getTemplateArgs().asArray(), nested) + + ">"); + } + if (parent) + return with(outer + record->getNameAsString()); + } + return with(writtenType(*context_, type)); + } + + std::string argumentsKey(llvm::ArrayRef arguments, + bool& nested) + { + std::string out; + for (const clang::TemplateArgument& argument : arguments) + { + if (!out.empty()) + out += ", "; + if (argument.getKind() == clang::TemplateArgument::Type) + out += typeKey(argument.getAsType(), nested); + else if (argument.getKind() == clang::TemplateArgument::Pack) + out += "{" + argumentsKey(argument.getPackAsArray(), nested) + "}"; + else + { + llvm::raw_string_ostream text(out); + argument.print(clang::PrintingPolicy(context_->getLangOpts()), text, true); + } + } + return out; + } + + // The identities of the functions the module emits (§5.7, step 5), read before the AST + // goes: a member of a nested type, a specialization whose arguments or type hold a + // nested type, an implicit member of a class whose key holds one. + void identify() + { + if (!generator_ || !collectFacts_ || !generator_->GetModule()) + return; + for (const llvm::Function& emitted : *generator_->GetModule()) + { + if (!emitted.hasName()) + continue; + const std::string symbol = emitted.getName().str(); + const auto* function = llvm::dyn_cast_or_null( + generator_->GetDeclForMangledName(symbol)); + if (!function) + continue; + const std::vector& symbols = symbolsOf(*function); + const std::vector keys = variantKeys(*function); + std::string variant; + for (std::size_t k = 0; k < symbols.size() && k < keys.size(); ++k) + { + if (symbols[k] == symbol) + variant = keys[k]; + } + UnitFacts::Identity identity{symbol, "", "", ""}; + if (const auto* method = llvm::dyn_cast(function)) + { + const clang::CXXRecordDecl* record = method->getParent(); + const std::string enclosing = record->isLambda() || record->isLocalClass() + ? enclosingOf(*record) + : ""; + if (!enclosing.empty()) + { + // The member's own type tells overloads apart, such as a local + // class's implicit default and move constructors, at one site. + bool member = false; + identity.nested = + std::string(record->isLambda() ? "lambda " : "method ") + variant + + "|" + textOf((*sites_)(record->getLocation())) + "|" + + textOf((*sites_)(method->getLocation())) + "|" + + method->getNameAsString() + + (method->isStatic() ? " static " : " ") + + method->getMethodQualifiers().getAsString() + "|" + + typeKey(method->getType(), member); + identity.enclosing = enclosing; + } + } + if (const clang::FunctionDecl* pattern = + function->getTemplateInstantiationPattern(/*ForDefinition=*/false)) + { + bool nested = false; + std::string arguments; + std::vector chain; + for (const clang::DeclContext* at = function->getDeclContext(); at; + at = at->getParent()) + chain.push_back(at); + for (auto at = chain.rbegin(); at != chain.rend(); ++at) + { + if (const auto* specialization = + llvm::dyn_cast(*at)) + { + arguments += + "<" + + argumentsKey(specialization->getTemplateArgs().asArray(), + nested) + + ">"; + } + } + if (const clang::TemplateArgumentList* own = + function->getTemplateSpecializationArgs()) + arguments += "(" + argumentsKey(own->asArray(), nested) + ")"; + // Its type too, so that a friend defined in a class template, which has + // neither arguments nor an enclosing specialization, is identified. + arguments += "[" + typeKey(function->getType(), nested) + "]"; + if (nested) + { + identity.specialization = variant + "|" + + textOf((*sites_)(pattern->getLocation())) + + "|" + arguments; + } + } + // An implicit member has no pattern and, outside a nested type, no nested + // key; when its class's key holds a nested type, it is identified by that + // class, its kind and name, its variant and its whole type, its qualifiers + // included. Nothing in it is specific to a standard library. + if (identity.nested.empty() && identity.specialization.empty()) + { + const auto* method = llvm::dyn_cast(function); + if (method && method->isImplicit()) + { + bool nested = false; + const std::string owner = + typeKey(context_->getRecordType(method->getParent()), nested); + if (nested) + { + const char* kind = + llvm::isa(method) ? "constructor" + : llvm::isa(method) ? "destructor" + : "method"; + const char* reference = + method->getRefQualifier() == clang::RQ_LValue ? " &" + : method->getRefQualifier() == clang::RQ_RValue ? " &&" + : ""; + bool inType = false; + identity.specialization = + "implicit " + variant + "|" + owner + "|" + kind + " " + + method->getNameAsString() + "|" + + method->getMethodQualifiers().getAsString() + reference + "|" + + typeKey(method->getType(), inType); + } + } + } + if (!identity.nested.empty() || !identity.specialization.empty()) + out_.identities.push_back(std::move(identity)); + } + } + + // Records nested symbols, each with its kind, for the targets whose entity they lie in. + void nested(const clang::DeclContext* context, clang::SourceLocation at, + const std::vector>& symbols) + { + if (at.isInvalid()) + return; + for (std::size_t i = 0; i < targets_.size(); ++i) + { + const std::string enclosing = enclosingSymbol(i, context); + if (enclosing.empty()) + continue; + const std::string site = textOf((*sites_)(at)); + for (const auto& [symbol, kind] : symbols) + out_.entities[i].nested.push_back({symbol, kind + "|" + site, enclosing}); + } + } + + // The constructors that inherit the entity of a target, or a specialization of its + // template (ruling 7): by derived class, inherited specialization and variant. + void inheritingIn(const clang::CXXRecordDecl& record) + { + for (const clang::CXXConstructorDecl* constructor : record.ctors()) + { + if (!constructor->isInheritingConstructor()) + continue; + const clang::CXXConstructorDecl* inherited = + constructor->getInheritedConstructor().getConstructor(); + if (!inherited) + continue; + std::string derived; + { + llvm::raw_string_ostream out(derived); + mangle_->mangleCanonicalTypeName(context_->getRecordType(&record), out); + } + const std::string arguments = argumentsOf(*context_, *inherited); + const std::vector& symbols = symbolsOf(*constructor); + const std::vector keys = variantKeys(*constructor); + // The specialization it inherits too: only its implicit code calls it, so no + // reference pairs it. + const std::vector& inheritedSymbols = symbolsOf(*inherited); + const std::vector inheritedKeys = variantKeys(*inherited); + for (std::size_t i = 0; i < targets_.size(); ++i) + { + if (!atSites(i, *inherited)) + continue; + for (std::size_t k = 0; k < symbols.size() && k < keys.size(); ++k) + { + out_.entities[i].inheriting.push_back( + {symbols[k], derived + "|" + arguments + "|" + keys[k], ""}); + } + for (std::size_t k = 0; + k < inheritedSymbols.size() && k < inheritedKeys.size(); ++k) + { + out_.entities[i].inheriting.push_back( + {inheritedSymbols[k], + "inherited|" + arguments + "|" + inheritedKeys[k], ""}); + } + } + } + } + + const EntityIdentity& identity(std::size_t i) const + { + return targets_[i].plan ? targets_[i].plan->entity : out_.entities[i].plan.entity; + } + + // §5.10, after the rewrite: each symbol the entity of target i takes that this + // compilation defines. Another entity's definition is a collision, with its IR + // linkage; the entity's own, such as the inline function of a rewritten header that + // several files define, is none. + void definitions(std::size_t i) + { + UnitFacts::Entity& facts = out_.entities[i]; + const llvm::Module* module = generator_->GetModule(); + std::vector takes = targets_[i].takes; + for (const const_suggestion::Named& variant : facts.variants) + { + if (std::find(takes.begin(), takes.end(), variant.symbol) == takes.end()) + takes.push_back(variant.symbol); + } + for (const std::string& symbol : takes) + { + const llvm::GlobalValue* value = + module ? module->getNamedValue(symbol) : nullptr; + if (!value || value->isDeclaration()) + continue; + if (!facts.linkage) + facts.linkage = value->getLinkage(); + const auto* function = llvm::dyn_cast_or_null( + generator_->GetDeclForMangledName(symbol)); + const clang::FunctionDecl* definition = + function ? function->getDefinition() : nullptr; + if (definition && atSites(i, *definition)) + continue; + const clang::SourceLocation at = + definition ? definition->getLocation() : clang::SourceLocation(); + facts.collisions.push_back({symbol, (*sites_)(at), value->getLinkage(), + at.isValid() ? lineOf(at) : 0}); + } + } + + // In the defining unit's initial compilation, the function is the one CodeGen emits + // under the suggestion's symbol. + void planAt(std::size_t i) + { + if (!generator_) + return; + const auto* function = llvm::dyn_cast_or_null( + generator_->GetDeclForMangledName(targets_[i].symbol)); + located_[i] = function; + if (!function) + return; + UnitFacts::Entity& facts = out_.entities[i]; + facts.plan = const_suggestion::plan(*context_, out_.macros, *function, + targets_[i].sourceIndex); + facts.plan.entity.symbols = variantSymbols(*generator_, *function); + facts.plan.entity.templateArguments = argumentsOf(*context_, *function); + } + + // The target's function in this compilation, and after the rewrite T v1's checks of + // its declarations. + void resolve(std::size_t i) + { + if (!generator_) + return; + const Target& target = targets_[i]; + const EntityIdentity& entity = identity(i); + UnitFacts::Entity& facts = out_.entities[i]; + std::vector found; // canonical declarations + const auto add = [&](const clang::FunctionDecl* function) + { + const clang::FunctionDecl* canonical = function->getCanonicalDecl(); + if (std::find(found.begin(), found.end(), canonical) == found.end()) + found.push_back(canonical); + }; + const auto named = [&](const clang::FunctionDecl* function) + { + const std::vector& symbols = symbolsOf(*function); + return std::find_first_of(symbols.begin(), symbols.end(), + entity.symbols.begin(), + entity.symbols.end()) != symbols.end(); + }; + if (!target.plan) + { + if (located_[i]) + add(located_[i]); + } + else if (!atSites_[i].empty()) + { + // Its declarations have its sites. A specialization has its pattern's, as the + // other specializations of its template: it is the one with its symbol, or + // once the rewrite renames it, the only one with its template arguments + // (ruling 7). + std::vector sameArguments; + for (const clang::FunctionDecl* function : atSites_[i]) + { + if (function->isDependentContext()) + continue; + if (!entity.templateOrigin || named(function)) + { + add(function); + } + else if (!entity.templateArguments.empty() && + argumentsOf(*context_, *function) == entity.templateArguments && + std::find(sameArguments.begin(), sameArguments.end(), + function->getCanonicalDecl()) == sameArguments.end()) + { + sameArguments.push_back(function->getCanonicalDecl()); + } + } + if (found.empty() && sameArguments.size() == 1) + add(sameArguments.front()); + } + else + { + // A unit that shares none of its sites declares it only through the linker. + for (const clang::FunctionDecl* function : linked_[i]) + { + if (!function->isDependentContext() && named(function)) + add(function); + } + } + facts.found = !found.empty(); + if (facts.found) + { + const std::vector& symbols = symbolsOf(*found.front()); + const std::vector keys = variantKeys(*found.front()); + for (std::size_t k = 0; k < symbols.size() && k < keys.size(); ++k) + facts.variants.push_back({symbols[k], keys[k], ""}); + facts.printedSignature = clang::PredefinedExpr::ComputeName( + clang::PredefinedIdentKind::PrettyFunction, found.front()); + for (const clang::FunctionDecl* function : found) + { + for (const clang::FunctionDecl* declaration : function->redecls()) + { + const clang::SourceLocation at = declaration->getLocation(); + if (at.isValid()) + facts.declarations.emplace((*sites_)(at), lineOf(at)); + } + } + } + // Before the rewrite, the type each declaration must get: the candidate of its + // own parameter type, as this compilation's language writes it (D8, D18). + const SuggestionPlan& plan = target.plan ? *target.plan : facts.plan; + if (!rewrittenBy_ && !plan.indeterminate) + { + for (const clang::FunctionDecl* function : found) + { + for (const clang::FunctionDecl* declaration : function->redecls()) + { + if (declaration->isDependentContext() || + plan.sourceIndex >= declaration->getNumParams()) + { + continue; + } + const clang::QualType candidate = candidateOf( + *context_, declaration->getParamDecl(plan.sourceIndex)->getType()); + if (!candidate.isNull()) + { + facts.expected.emplace((*sites_)(declaration->getLocation()), + writtenType(*context_, candidate)); + } + } + } + } + if (!target.plan || target.plan != rewrittenBy_) + return; + definitions(i); + if (!facts.found) + return; + const clang::SourceManager& sm = context_->getSourceManager(); + for (const clang::FunctionDecl* function : found) + { + for (const clang::FunctionDecl* declaration : function->redecls()) + { + if (declaration->isDependentContext() || + target.plan->sourceIndex >= declaration->getNumParams()) + { + continue; + } + const std::string type = writtenType( + *context_, + declaration->getParamDecl(target.plan->sourceIndex)->getType()); + // A declaration with no expected type was another entity's before the + // rewrite, which merges into it: the collisions judge it (ruling 3). + const auto expected = + target.expected.find((*sites_)(declaration->getLocation())); + if (expected == target.expected.end() || type == expected->second) + continue; + const clang::SourceLocation at = + sm.getExpansionLoc(declaration->getLocation()); + const std::string& file = sites_->fileOf(sm.getFileID(at)); + // A planned parameter must have the candidate of its own type, as its + // unit writes it (§5.6, step 3; D8, D18). A declaration in a file that the + // plan does not rewrite, through which the unit declares the entity for the + // linker only, keeps its type: the unit does not see the rewrite (D17). + if (target.plan->edits.count(file)) + facts.withoutCandidate = true; + else + facts.stale = true; + } + } + // The explicit casts of its address, in source order (D15). + std::vector places; + for (const auto& [function, at] : casts_) + { + if (std::find(found.begin(), found.end(), function) != found.end()) + places.push_back(sm.getExpansionLoc(at)); + } + std::stable_sort(places.begin(), places.end(), + [&](clang::SourceLocation a, clang::SourceLocation b) + { return sm.isBeforeInTranslationUnit(a, b); }); + for (const clang::SourceLocation at : places) + facts.casts.push_back({(*sites_)(at), lineOf(at)}); + } + + bool collectFacts_ = false; + std::uint8_t reservedPadding_[6] = {}; + clang::CodeGenAction& action_; + const std::vector& targets_; + const SuggestionPlan* rewrittenBy_; + UnitFacts& out_; + clang::ASTContext* context_ = nullptr; + clang::CodeGenerator* generator_ = nullptr; + std::optional sites_; + std::unique_ptr mangle_; // for what CodeGen does not name + std::set declarationFiles_; + std::set rewrittenFiles_; + std::map names_; + std::vector located_; + std::vector> atSites_; // with one of its sites + std::vector> linked_; // and the linker's + // The functions whose address is cast explicitly, other than to void, and where. + std::vector> casts_; + std::vector contexts_; // the instantiations around, outermost first + std::map> symbols_; + std::map> values_; + std::map shown_; + // After the rewrite, the planned parameter's type being traversed (D19). + const clang::TypeSourceInfo* plannedType_ = nullptr; + std::size_t inPlannedType_ = 0; + std::map rewrittenSpelling_; // by FileID: is it a rewritten file + }; + + class InspectAction : public clang::EmitLLVMOnlyAction + { + public: + InspectAction(llvm::LLVMContext* context, const std::vector& targets, + const SuggestionPlan* rewrittenBy, UnitFacts& out) + : clang::EmitLLVMOnlyAction(context), targets_(targets), rewrittenBy_(rewrittenBy), + out_(out) + { + } + + protected: + std::unique_ptr CreateASTConsumer(clang::CompilerInstance& ci, + llvm::StringRef file) override + { + std::unique_ptr generator = + clang::EmitLLVMOnlyAction::CreateASTConsumer(ci, file); + if (!generator) + return nullptr; + std::vector> consumers; + consumers.push_back(std::move(generator)); + consumers.push_back( + std::make_unique(*this, targets_, rewrittenBy_, out_)); + if (std::any_of(targets_.begin(), targets_.end(), + [](const Target& target) { return !target.plan; })) + { + ci.getPreprocessor().addPPCallbacks(const_suggestion::recordMacroDefinitions( + ci.getSourceManager(), out_.macros)); + } + return std::make_unique(std::move(consumers)); + } + + private: + const std::vector& targets_; + const SuggestionPlan* rewrittenBy_; + UnitFacts& out_; + }; + + // An import declaration (`import M;`, `#pragma clang module import`), which gives no + // token of its own when it names a C++20 module. + class ImportRecorder : public clang::PPCallbacks + { + public: + explicit ImportRecorder(bool& imports) : imports_(imports) {} + + void moduleImport(clang::SourceLocation, clang::ModuleIdPath, + const clang::Module*) override + { + imports_ = true; + } + + private: + bool& imports_; + }; + + // Preprocesses a unit and records every identifier, and the keyword `operator` that an + // operator's name begins with, after macro expansion. System headers are read too: a + // declaration that one of them makes is the unit's own. A module that the unit imports + // is not read: the import is recorded, from its token (an #include that becomes an + // import, a header unit), from its declaration, or from the unit's own module + // declaration when the unit implements a module, whose interface it imports. + class ScanAction : public clang::PreprocessorFrontendAction + { + public: + explicit ScanAction(UnitScan& out) : out_(out) {} + + protected: + void ExecuteAction() override + { + clang::Preprocessor& preprocessor = getCompilerInstance().getPreprocessor(); + preprocessor.addPPCallbacks(std::make_unique(out_.importsModule)); + preprocessor.EnterMainSourceFile(); + clang::Token token; + do + { + preprocessor.Lex(token); + if (token.is(clang::tok::identifier)) + out_.identifiers.insert( + llvm::xxh3_64bits(token.getIdentifierInfo()->getName())); + else if (token.is(clang::tok::kw_operator)) + out_.identifiers.insert(llvm::xxh3_64bits(llvm::StringRef("operator"))); + else if (token.isOneOf(clang::tok::annot_module_include, + clang::tok::annot_header_unit)) + out_.importsModule = true; + } while (token.isNot(clang::tok::eof)); + if (preprocessor.isInImplementationUnit()) + out_.importsModule = true; + } + + private: + UnitScan& out_; + }; + } // namespace + + UnitFacts const_suggestion::inspect(const UnitCommand& command, + const SuggestionPlan* rewrittenBy, + const std::map& rewritten, + const std::vector& targets, std::string_view step, + bool timing) + { + const analyzer::ScopedHotspot hotspot(timing, step); + static std::once_flag targetsReady; + std::call_once(targetsReady, + [] + { + llvm::InitializeAllTargetInfos(); + llvm::InitializeAllTargets(); + llvm::InitializeAllTargetMCs(); + }); + UnitFacts out; + out.timing = timing; + std::string error; + const bool ran = runSerializedCompile( + command.workingDir, + [&] + { + llvm::IntrusiveRefCntPtr files = + llvm::vfs::getRealFileSystem(); + DiagnosticCollector collector(out); + std::vector argv; + for (const std::string& arg : command.args) + argv.push_back(arg.c_str()); + clang::CreateInvocationOptions options; + options.Diags = clang::CompilerInstance::createDiagnostics( + *files, new clang::DiagnosticOptions(), &collector, false); + options.VFS = files; + options.ProbePrecompiled = false; + std::unique_ptr invocation = + clang::createInvocation(argv, options); + if (!invocation) + { + out.failure = "the compile command is not understood: " + out.firstDiagnostic; + return; + } + // Nothing is written: no dependency file, no output. + invocation->getDependencyOutputOpts() = clang::DependencyOutputOptions(); + // The inspection reads the AST after CodeGen: keep it for the backend. + invocation->getCodeGenOpts().ClearASTBeforeBackend = false; + // Values and blocks numbered, not named: a block that inlining names after + // the inlined function carries the rewritten function's symbol. + invocation->getCodeGenOpts().DiscardValueNames = true; + // The comparison removes debug information: do not generate it. + invocation->getCodeGenOpts().setDebugInfo(llvm::codegenoptions::NoDebugInfo); + clang::DiagnosticOptions& diagnostics = invocation->getDiagnosticOpts(); + // No "N warnings generated" on the analyzer's output. + diagnostics.ShowCarets = false; + diagnostics.IgnoreWarnings = false; + // No error limit: "too many errors" is fatal, and would leave unchecked a rewrite + // that its errors refute (ruling 12). + diagnostics.ErrorLimit = 0; + // D6: each tracked group as Clang reports it by default, whatever the command + // says: enabled, a warning under -Werror, and an error when its diagnostics are + // errors by default (incompatible-function-pointer-types, int-conversion in C). + const clang::DiagnosticIDs& ids = *options.Diags->getDiagnosticIDs(); + std::vector errors; + for (const char* warning : kCheckedWarnings) + { + diagnostics.Warnings.push_back(warning); + diagnostics.Warnings.push_back(std::string("no-error=") + warning); + llvm::SmallVector members; + if (!ids.getDiagnosticsInGroup(clang::diag::Flavor::WarningOrError, warning, + members) && + std::all_of(members.begin(), members.end(), [&](clang::diag::kind id) + { return ids.isDefaultMappingAsError(id); })) + { + errors.push_back(std::string("error=") + warning); + } + } + diagnostics.Warnings.insert(diagnostics.Warnings.end(), errors.begin(), + errors.end()); + out.diagnostics.clear(); + out.reported.clear(); + out.firstDiagnostic.clear(); + + clang::CompilerInstance instance; + instance.setInvocation(std::move(invocation)); + instance.createDiagnostics(*files, &collector, false); + instance.createFileManager(files); + for (const auto& [path, text] : rewritten) + { + instance.getPreprocessorOpts().addRemappedFile( + path, llvm::MemoryBuffer::getMemBufferCopy(text, path).release()); + } + instance.getPreprocessorOpts().RetainRemappedFileBuffers = false; + instance.getFrontendOpts().DisableFree = false; + instance.getCodeGenOpts().DisableFree = false; + out.context = std::make_unique(); + InspectAction action(out.context.get(), targets, rewrittenBy, out); + instance.ExecuteAction(action); + out.module = action.takeModule(); + }, + error); + if (!ran) + out.failure = error; + return out; + } + + // The files a unit reads and its identifiers, from its preprocessing. Empty when the + // unit cannot be preprocessed without error: what it reads is then unknown. + UnitScan const_suggestion::scan(const UnitCommand& command, bool timing) + { + const analyzer::ScopedHotspot hotspot(timing, "const_suggestion.preprocess"); + UnitScan out; + std::string error; + runSerializedCompile( + command.workingDir, + [&] + { + llvm::IntrusiveRefCntPtr files = + llvm::vfs::getRealFileSystem(); + clang::IgnoringDiagConsumer ignore; + std::vector argv; + for (const std::string& arg : command.args) + argv.push_back(arg.c_str()); + clang::CreateInvocationOptions options; + options.Diags = clang::CompilerInstance::createDiagnostics( + *files, new clang::DiagnosticOptions(), &ignore, false); + options.VFS = files; + options.ProbePrecompiled = false; + std::unique_ptr invocation = + clang::createInvocation(argv, options); + if (!invocation) + return; + invocation->getDependencyOutputOpts() = clang::DependencyOutputOptions(); + invocation->getDiagnosticOpts().ShowCarets = false; + clang::CompilerInstance instance; + instance.setInvocation(std::move(invocation)); + instance.createDiagnostics(*files, &ignore, false); + instance.createFileManager(files); + ScanAction action(out); + instance.ExecuteAction(action); + if (!instance.hasSourceManager() || instance.getDiagnostics().hasErrorOccurred()) + return; + const clang::SourceManager& sm = instance.getSourceManager(); + for (auto it = sm.fileinfo_begin(); it != sm.fileinfo_end(); ++it) + out.files.insert(realPath(it->first.getName())); + }, + error); + return out; + } + + const_suggestion::SiteReader::SiteReader(const clang::SourceManager& sm, + const SuggestionPlan* rewrittenBy) + : sm_(sm) + { + if (!rewrittenBy) + return; + edits_ = rewrittenBy->edits; + for (auto& [path, edits] : edits_) + { + std::sort(edits.begin(), edits.end(), + [](const Edit& a, const Edit& b) { return a.offset < b.offset; }); + } + } + + DeclarationSite const_suggestion::SiteReader::operator()(clang::SourceLocation location) + { + if (location.isInvalid()) + return {}; + return {position(sm_.getExpansionLoc(location)), position(sm_.getSpellingLoc(location))}; + } + + const std::string& const_suggestion::SiteReader::fileOf(clang::FileID file) + { + const auto found = files_.find(file); + if (found != files_.end()) + return found->second; + const clang::OptionalFileEntryRef entry = sm_.getFileEntryRefForID(file); + std::string path = entry ? realPath(entry->getName()) + : sm_.getBufferName(sm_.getLocForStartOfFile(file)).str(); + return files_.emplace(file, std::move(path)).first->second; + } + + bool const_suggestion::SiteReader::edited(const SourcePosition& at) const + { + const auto edits = edits_.find(at.file); + if (edits == edits_.end()) + return false; + return std::any_of(edits->second.begin(), edits->second.end(), + [&](const Edit& edit) + { + return edit.offset <= at.offset && + at.offset < + edit.offset + std::max(edit.length, 1); + }); + } + + SourcePosition const_suggestion::SiteReader::position(clang::SourceLocation location) + { + const auto [file, offset] = sm_.getDecomposedLoc(location); + SourcePosition out{fileOf(file), offset}; + const auto edits = edits_.find(out.file); + if (edits != edits_.end()) + out.offset = originalOffset(edits->second, out.offset); + return out; + } +} // namespace ctrace::stack::analysis diff --git a/src/analysis/ConstSuggestionInternal.hpp b/src/analysis/ConstSuggestionInternal.hpp new file mode 100644 index 0000000..379cc58 --- /dev/null +++ b/src/analysis/ConstSuggestionInternal.hpp @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "analysis/ConstSuggestionCheck.hpp" + +namespace clang +{ + class ASTContext; + class FunctionDecl; + class PPCallbacks; + class QualType; + class SourceManager; +} // namespace clang + +namespace llvm +{ + class LLVMContext; + class Module; +} // namespace llvm + +namespace ctrace::stack::analysis +{ + struct PrintedSignature; +} // namespace ctrace::stack::analysis + +namespace ctrace::stack::analysis::const_suggestion +{ + // What the check concludes about a suggestion, in one unit or overall (#217 v2, §3). + enum class Verdict : std::uint8_t + { + Verified, + Refuted, + Indeterminate + }; + + struct Judgement + { + std::string reason; // empty when verified + Verdict verdict = Verdict::Verified; + std::uint8_t reservedPadding[7] = {}; + }; + + // Refuted if a judgement refutes, else indeterminate if one is, else verified; with the + // reason of the first judgement that has that verdict. + Judgement aggregate(const std::vector& judgements); + + // 16 hexadecimal digits: xxh3 of the arguments and the working directory, each followed by a + // NUL byte. + std::string fingerprintOf(const UnitCommand& command); + + // A file's real path, or the path as given when it has none. + std::string realPath(llvm::StringRef path); + + // `length` bytes at `offset` replaced with `text`. + struct Edit + { + std::size_t offset = 0; + std::size_t length = 0; + std::string text; + unsigned line = 0; // the offset's, in the original text + std::uint8_t reservedPadding[4] = {}; + }; + + // The type a suggestion tries (§3), as the trace shows it: the parameter's current type and + // the candidate, each written with the parameter's name, and the alternative it stands for. + struct Candidate + { + std::string current; + std::string type; + std::string alternative; + // The candidate as a diagnostic displays it: Clang's writing, a type without a name + // written (stage 2, ruling 3). + std::string shown; + }; + + // A position in a file's original text: after the rewrite, a position in a rewritten file is + // mapped back through the plan's edits (§5.4). + struct SourcePosition + { + std::string file; // real path + std::size_t offset = 0; + bool operator==(const SourcePosition&) const = default; + bool operator<(const SourcePosition& other) const + { + return std::tie(file, offset) < std::tie(other.file, other.offset); + } + }; + + // Where a name is expanded and where it is written: one macro invocation can expand several + // names at the same place (ruling 14). + struct DeclarationSite + { + SourcePosition expansion; + SourcePosition spelling; + bool operator==(const DeclarationSite&) const = default; + bool operator<(const DeclarationSite& other) const + { + return std::tie(expansion, spelling) < std::tie(other.expansion, other.spelling); + } + }; + + enum class SourceLinkage : std::uint8_t + { + None, + Internal, + External + }; + + // The function a suggestion names, as the defining unit's initial compilation shows it (§3). + struct EntityIdentity + { + std::vector redeclarations; // the declarations the plan rewrites, sorted + std::string name; // as Clang prints it: dot, operator==, ~A + std::optional templateOrigin; // a specialization's pattern, first declared + std::vector symbols; // every variant's, before the rewrite (CodeGen) + std::string templateArguments; // a specialization's, written canonically (ruling 7) + SourceLinkage linkage = SourceLinkage::None; + // Whether Clang's kind of the name is an operator's (operator==, operator""_x): such a + // name is the keyword `operator` and what follows it, never one identifier. A name is + // told by its kind, not its spelling: `operator$x` is an identifier. + bool operatorName = false; + std::uint8_t reservedPadding[6] = {}; + }; + + // R, O and K (§5.4). + enum class FactKind : std::uint8_t + { + Reference, + Override, + Constant + }; + + // Where a fact is: the instantiations around it, by their symbols, outermost first, and its + // site. + struct FactKey + { + std::vector contexts; + DeclarationSite site; + bool operator==(const FactKey&) const = default; + bool operator<(const FactKey& other) const + { + return std::tie(contexts, site) < std::tie(other.contexts, other.site); + } + }; + + // What one compilation shows at a key, and what a reason names when it changes (Texts). + struct Fact + { + FactKey key; + // Sorted. R: the target's declaration sites, or the symbol of a specialization or an + // implicit function (ruling 4); O: each overridden method, the same way; K: true, false + // or the selected association. + std::vector value; + std::string subject; // O: the method; K: _Generic for a selection + std::string shown; // the value, as a reason names it + unsigned line = 0; // the site's, where Clang expands it + FactKind kind = FactKind::Reference; + std::uint8_t reservedPadding[3] = {}; + }; + + // The rewrite of one suggestion, planned on the defining unit's initial compilation (§5.5). + struct SuggestionPlan + { + EntityIdentity entity; + std::vector parameters; // the parameters it rewrites, sorted + std::map> edits; // by real path + Candidate candidate; // the type the rewrite gives the parameter + std::optional indeterminate; // why the plan is not executed + unsigned sourceIndex = 0; + // A free function with external linkage can be declared by a file that reads none of the + // rewritten files; a method only where its class is defined. One with internal linkage + // (static, anonymous namespace) cannot: another file's homonym is another function. + bool declarableElsewhere = false; + std::uint8_t reservedPadding[3] = {}; + }; + + // The sites of one compilation's locations. After the rewrite, a position in a file that + // `rewrittenBy` rewrites is mapped back to the original text through its edits. + class SiteReader + { + public: + SiteReader(const clang::SourceManager& sm, const SuggestionPlan* rewrittenBy); + + DeclarationSite operator()(clang::SourceLocation location); + // The real path of a file, or its buffer's name when it is none. + const std::string& fileOf(clang::FileID file); + // Whether a position of the original text is written by one of the edits: inside the + // text an edit replaces, or where it inserts text (§5.6, D19). + bool edited(const SourcePosition& at) const; + + private: + SourcePosition position(clang::SourceLocation location); + + const clang::SourceManager& sm_; + std::map> edits_; // by real path, sorted by offset + std::map files_; + }; + + // External for external or module linkage, internal for internal or unique-external. + SourceLinkage linkageOf(const clang::FunctionDecl& function); + + // A type written canonically, an anonymous type without its location. + std::string writtenType(const clang::ASTContext& context, clang::QualType type); + + // The type a parameter of type `type` gets once rewritten (§3); null for another type. + clang::QualType candidateOf(clang::ASTContext& context, clang::QualType type); + + // The macro definitions a compilation reads, by file: from each macro's name to its last + // token, as offsets (D11). + using MacroDefinitions = std::map>>; + + // Records them, in the compilation that plans. + std::unique_ptr recordMacroDefinitions(const clang::SourceManager& sm, + MacroDefinitions& out); + + // Plans the rewrite of the parameter at `sourceIndex` of `function`, located in the defining + // unit's initial compilation by the suggestion's symbol. + SuggestionPlan plan(clang::ASTContext& context, const MacroDefinitions& macros, + const clang::FunctionDecl& function, std::optional sourceIndex); + + // The generated code of one module, ready to compare: what carries no meaning removed, and + // each piece keyed by what it defines. + struct ModuleText + { + std::map globals; // functions, variables, aliases + std::map types; + std::map attributeGroups; // "#N" -> "{ ... }" + std::map metadata; // "!N" -> its definition + std::map namedMetadata; // "!name" -> its operands + std::set defined; // has code or data here + }; + + // Prepares `module`, which it changes (debug information and other metadata removed). + ModuleText prepare(llvm::Module& module); + + // rewrittenCodeDifference, with the code before the rewrite already prepared. + std::optional codeDifference(const ModuleText& was, const ModuleText& now, + const std::vector& established, + const PrintedSignature* signature, + std::vector* accepted); + + // What to look for in one compilation: the function a suggestion names. + struct Target + { + std::string symbol; // the suggestion's symbol, before the rewrite + // Its plan; null in the defining unit's initial compilation, which plans it. + const SuggestionPlan* plan = nullptr; + // The parameter's position among those the source declares (P0), none for one it + // does not declare. + std::optional sourceIndex; + // After the rewrite, the symbols the entity takes, as the defining unit's compilation + // names them: another unit can define one without declaring the entity (§5.10). + std::vector takes; + // After the rewrite, the type each declaration of the entity must have, by site: the + // unit's own, from its compilation before the rewrite (D8, D18). + std::map expected; + }; + + // An error or a checked warning, in the order Clang reports it. + struct Diagnostic + { + std::string key; // place and kind, as `diagnostics` holds it + std::string text; + bool error = false; + std::uint8_t reservedPadding[7] = {}; + }; + + // A symbol that the rewrite may rename with the entity, and what pairs it (§5.7). + struct Named + { + std::string symbol; + std::string key; // its kind or variant, and what it belongs to + std::string enclosing; // a nested symbol's: the symbol of the function around it + }; + + // What one compilation shows (§5.3, §5.6). + struct UnitFacts + { + // About the function of one target. + struct Entity + { + // After the rewrite: a symbol the entity takes that another entity defines here, or + // another entity's declaration merged into it (ruling 3). + struct Collision + { + std::string symbol; + DeclarationSite site; // the other entity's definition, or declaration + std::optional linkage; // none for a declaration + unsigned line = 0; // the site's, where Clang expands it + std::uint8_t reservedPadding[4] = {}; + }; + + // After the rewrite, an explicit cast of its address, other than to void (D15). + struct Cast + { + DeclarationSite site; + unsigned line = 0; // the site's, where Clang expands it + std::uint8_t reservedPadding[4] = {}; + }; + + SuggestionPlan plan; // in the compilation that plans + std::string printedSignature; // for __PRETTY_FUNCTION__ + std::vector casts; // in source order + std::vector collisions; + // After the rewrite, the IR linkage of the first symbol it takes that this + // compilation defines: in the defining unit, the entity's own (§5.10). + std::optional linkage; + // Its declarations' sites, each with the line where Clang expands it: those that the + // rewrite adds are other entities' declarations that merge into it (ruling 3). + std::map declarations; + // Before the rewrite, by site, the type each of its declarations must get: the + // candidate of the declaration's own parameter type, its top-level qualifiers kept, + // written canonically in this compilation's language (D8, D18). + std::map expected; + std::vector overrides; // O, for the methods named like it + std::vector variants; // the entity's: C1, C2, D0... or "" for a function + // The constructors inheriting it, or a specialization of its template: by derived + // class, inherited specialization and variant (ruling 7). + std::vector inheriting; + // Lambdas, local classes (methods, _ZTI, _ZTS, _ZTV), static locals and their guards, + // in the entity or in a specialization of its template: by kind and site. + std::vector nested; + bool found = false; // the compilation declares it + // After the rewrite, a declaration in a rewritten file without the candidate. + bool withoutCandidate = false; + // After the rewrite, a declaration in a file it does not rewrite, without the + // candidate: the unit declares the entity only for the linker (D17). + bool stale = false; + std::uint8_t reservedPadding[5] = {}; + }; + + // A declaration that a rewritten file writes (§5.6). + struct Written + { + std::string type; // canonically, or its written form when a part is deduced (D19) + std::string name; + unsigned line = 0; // where Clang expands it + std::uint8_t reservedPadding[4] = {}; + }; + + bool timing = false; + std::uint8_t reservedPadding[7] = {}; + std::string failure; // the compilation could not run + std::string file; // its main file's real path + std::multiset diagnostics; + std::vector reported; + std::string firstDiagnostic; + std::string fatal; // the first fatal error: the AST lacks the rest of the unit + // In the rewritten files, by site; one site can hold several, in the order of the + // traversal, such as a macro argument expanded twice. + std::map> writtenTypes; + // After the rewrite, the first type that an edit writes outside the planned parameters' + // types, in a declaration or an expression, a default argument included (§5.6, D19). + struct Outside + { + DeclarationSite site; + unsigned line = 0; // where Clang expands it + std::uint8_t reservedPadding[4] = {}; + }; + std::optional writtenOutside; + // Each function the module emits that a nested type (a lambda or a local class) names, as + // Clang identifies it, read before the AST goes (§5.7, step 5). + struct Identity + { + std::string symbol; + std::string + nested; // member of a nested type: kind and variant, record and member sites + std::string enclosing; // the symbol of the function around that nested type + std::string + specialization; // variant, pattern site, template arguments with nested types + }; + std::vector identities; + std::unique_ptr context; + std::unique_ptr module; + std::vector facts; // R and K, in the order of the traversal + std::set instantiations; // the symbols of those around a fact + std::vector entities; // indexed like the targets + const_suggestion::MacroDefinitions macros; // read only when the compilation plans + }; + + // The item of an R value that starts with `prefix`, without it; "" when it has none. + std::string itemOf(const std::vector& value, std::string_view prefix); + + // The pairs established between a unit's initial and modified compilations for the entity + // at `entity` of `before` and the one of `after` (§5.7). `missing` receives the first reason + // a correspondence could not be established. + std::vector correspond(const UnitFacts& before, std::size_t entity, + const UnitFacts& after, std::string& missing); + + // §5.10: the verdict of the collisions that the units' compilations after the rewrite found, + // once none of the units refutes: `definingFile` names the defining unit, `function` the + // entity as the diagnostic shows it. + Judgement judgeCollisions(const std::vector& units, std::size_t entity, + const std::string& definingFile, const std::string& function); + + // Compiles a unit and inspects it for `targets`. After the rewrite, `rewritten` remaps the + // files that the plan `rewrittenBy` rewrites to their texts. + UnitFacts inspect(const UnitCommand& command, const SuggestionPlan* rewrittenBy, + const std::map& rewritten, + const std::vector& targets, std::string_view step, bool timing); + + // The text of each file that `plan` rewrites, its edits applied. `failure` names a file that + // cannot be read, or that has become too short for an edit, so changed after the plan was + // made; the result is then incomplete and must not be judged. A file that changed and is long + // enough is not detected. + std::map rewrite(const SuggestionPlan& plan, std::string& failure); + + // The files a unit reads and its identifiers, from its preprocessing. + UnitScan scan(const UnitCommand& command, bool timing); +} // namespace ctrace::stack::analysis::const_suggestion diff --git a/src/analysis/ConstSuggestionMatch.cpp b/src/analysis/ConstSuggestionMatch.cpp new file mode 100644 index 0000000..13f8103 --- /dev/null +++ b/src/analysis/ConstSuggestionMatch.cpp @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: Apache-2.0 +// The correspondences of a rewrite (#217, §5.7): which symbols of a unit's compilation after the +// rewrite are those of its compilation before it, established from compilation facts only. +#include "ConstSuggestionInternal.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ctrace::stack::analysis +{ + namespace + { + using const_suggestion::Fact; + using const_suggestion::FactKey; + using const_suggestion::FactKind; + using const_suggestion::itemOf; + using const_suggestion::Judgement; + using const_suggestion::Named; + using const_suggestion::UnitFacts; + using const_suggestion::Verdict; + + // Appends `item` once, in the order items come. + void addOnce(std::vector& items, const std::string& item) + { + if (std::find(items.begin(), items.end(), item) == items.end()) + items.push_back(item); + } + } // namespace + + std::string const_suggestion::itemOf(const std::vector& value, + std::string_view prefix) + { + for (const std::string& item : value) + { + if (item.starts_with(prefix)) + return item.substr(prefix.size()); + } + return ""; + } + + std::vector const_suggestion::correspond(const UnitFacts& before, + std::size_t entity, const UnitFacts& after, + std::string& missing) + { + std::map pairs; // before the rewrite, to after it + std::set taken; // the symbols after it, once paired + const auto pair = [&](const std::string& was, const std::string& now) + { + if (was.empty() || now.empty() || pairs.count(was) || taken.count(now)) + return false; + pairs[was] = now; + taken.insert(now); + return true; + }; + const UnitFacts::Entity* was = + entity < before.entities.size() ? &before.entities[entity] : nullptr; + const UnitFacts::Entity* now = after.entities.empty() ? nullptr : &after.entities[0]; + + // 1 and 2: the entity's variants, by variant; the constructors inheriting it, by derived + // class, inherited specialization and variant. + const auto byKey = + [&](const std::vector& wasNamed, const std::vector& nowNamed) + { + for (const Named& one : wasNamed) + { + const auto other = + std::find_if(nowNamed.begin(), nowNamed.end(), + [&](const Named& named) { return named.key == one.key; }); + if (other != nowNamed.end()) + pair(one.symbol, other->symbol); + } + }; + if (was && now) + { + byKey(was->variants, now->variants); + byKey(was->inheriting, now->inheriting); + } + + // 3: the instantiation contexts. A reference whose instantiations already map, and whose + // target becomes another specialization of the same template, proposes that pair; the + // proposals of every reference are checked together, and each new pair maps more + // instantiations, until none is added. + // The references at each key, in the order of the traversal (Task 3.2, O1): those after + // the rewrite, and those before it at a key without instantiation context, which no pair + // changes, are grouped once; the others go through each round's pairs. + struct At + { + std::size_t first = 0; // the position of its first reference before the rewrite + FactKey key; + std::vector facts; + }; + std::map> nowAt; + for (const Fact& fact : after.facts) + { + if (fact.kind == FactKind::Reference) + nowAt[fact.key].push_back(&fact); + } + std::vector fixed; + std::vector> contextual; + { + std::map slot; + for (std::size_t i = 0; i < before.facts.size(); ++i) + { + const Fact& fact = before.facts[i]; + if (fact.kind != FactKind::Reference) + continue; + if (!fact.key.contexts.empty()) + { + contextual.emplace_back(i, &fact); + continue; + } + const auto [it, added] = slot.try_emplace(fact.key, fixed.size()); + if (added) + fixed.push_back({i, fact.key, {}}); + fixed[it->second].facts.push_back(&fact); + } + } + // Steps 3, 4 and 5 run to one common fixpoint: a pair that one establishes may map what + // another needs. + for (std::size_t settled = static_cast(-1); settled != pairs.size();) + { + settled = pairs.size(); + for (bool added = true; added;) + { + added = false; + const auto forward = [&](FactKey key) -> std::optional + { + for (std::string& context : key.contexts) + { + const auto found = pairs.find(context); + if (found != pairs.end()) + context = found->second; + else if (!after.instantiations.count(context)) + return std::nullopt; + } + return key; + }; + std::vector moving; + { + std::map slot; + for (const auto& [i, fact] : contextual) + { + if (const std::optional key = forward(fact->key)) + { + const auto [it, added] = slot.try_emplace(*key, moving.size()); + if (added) + moving.push_back({i, *key, {}}); + moving[it->second].facts.push_back(fact); + } + } + } + // Both, merged in the order of their first references: a key with contexts never + // equals one without. + std::vector order; + order.reserve(fixed.size() + moving.size()); + for (std::size_t a = 0, b = 0; a < fixed.size() || b < moving.size();) + { + if (b == moving.size() || + (a < fixed.size() && fixed[a].first < moving[b].first)) + order.push_back(&fixed[a++]); + else + order.push_back(&moving[b++]); + } + std::vector proposing; // in the order of the facts + std::map> to; // before to after + std::map> from; // after to before + std::map shown; + for (const At* at : order) + { + const std::vector& wasFacts = at->facts; + const auto nowFacts = nowAt.find(at->key); + if (nowFacts == nowAt.end() || nowFacts->second.size() != wasFacts.size()) + continue; + for (std::size_t k = 0; k < wasFacts.size(); ++k) + { + const Fact& one = *wasFacts[k]; + const Fact& other = *nowFacts->second[k]; + const std::string wasTarget = itemOf(one.value, "symbol:"); + const std::string nowTarget = itemOf(other.value, "symbol:"); + const std::string pattern = itemOf(one.value, "template:"); + if (wasTarget.empty() || nowTarget.empty() || pattern.empty() || + wasTarget == nowTarget || pattern != itemOf(other.value, "template:") || + pairs.count(wasTarget)) + { + continue; + } + addOnce(proposing, wasTarget); + addOnce(to[wasTarget], nowTarget); + addOnce(from[nowTarget], wasTarget); + shown[wasTarget] = one.shown; + shown[nowTarget] = other.shown; + } + } + for (const std::string& one : proposing) + { + const std::vector& targets = to[one]; + if (targets.size() > 1) + { + if (missing.empty()) + { + missing = "'" + shown[one] + "' becomes both '" + shown[targets[0]] + + "' and '" + shown[targets[1]] + + "': their correspondence is not one to one"; + } + continue; + } + // Two before the rewrite that become one, or one that becomes a symbol already + // paired. + std::vector sources = from[targets[0]]; + for (const auto& [wasSymbol, nowSymbol] : pairs) + { + if (nowSymbol == targets[0]) + sources.insert(sources.begin(), wasSymbol); + } + if (sources.size() > 1) + { + if (missing.empty()) + { + const auto name = [&](const std::string& symbol) + { + return shown.count(symbol) ? shown[symbol] : llvm::demangle(symbol); + }; + missing = "'" + name(sources[0]) + "' and '" + name(sources[1]) + + "' both become '" + name(targets[0]) + + "': their correspondence is not one to one"; + } + continue; + } + added = pair(one, targets[0]) || added; + } + } + + // 4: the nested symbols, by kind and site, once the symbols around them are paired. + for (bool added = was && now; added;) + { + added = false; + for (const Named& one : was->nested) + { + if (pairs.count(one.symbol)) + continue; + const auto around = pairs.find(one.enclosing); + const std::string enclosing = + around == pairs.end() ? one.enclosing : around->second; + const auto other = std::find_if( + now->nested.begin(), now->nested.end(), [&](const Named& named) + { return named.key == one.key && named.enclosing == enclosing; }); + if (other != now->nested.end()) + added = pair(one.symbol, other->symbol) || added; + } + } + + // 5: the types nested in every established pair (lambdas, local classes) and what they + // reach: the members of those types, the specializations whose arguments or type hold + // them, and the implicit members of the classes that hold them, by the identities the + // facts give (UnitFacts::Identity). A key's nested types are mapped through the pairs; + // every other part of it is kept as it is. One to one: a key with more than one symbol + // on either side gives no pair, and a reason. + { + const auto substitute = [&](const std::string& key) + { + std::string out; + for (std::size_t at = 0; at < key.size();) + { + const std::size_t open = key.find('\x02', at); + if (open == std::string::npos) + { + out += key.substr(at); + break; + } + const std::size_t close = key.find('\x03', open); + out += key.substr(at, open + 1 - at); + const std::string enclosing = key.substr(open + 1, close - open - 1); + const auto mapped = pairs.find(enclosing); + out += mapped == pairs.end() ? enclosing : mapped->second; + at = close; + } + return out; + }; + // Only the symbols that the rewrite renames: one side has them, the other not. + std::set beforeNames, afterNames; + for (const UnitFacts::Identity& identity : before.identities) + beforeNames.insert(identity.symbol); + for (const UnitFacts::Identity& identity : after.identities) + afterNames.insert(identity.symbol); + std::map> nowAtKey; // key -> symbols after + for (const UnitFacts::Identity& identity : after.identities) + { + if (taken.count(identity.symbol) || beforeNames.count(identity.symbol)) + continue; + if (!identity.nested.empty()) + nowAtKey["n" + identity.nested + '\0' + identity.enclosing].push_back( + identity.symbol); + if (!identity.specialization.empty()) + nowAtKey["s" + identity.specialization].push_back(identity.symbol); + } + std::map> + wasAtKey; // mapped key -> symbols before + for (const UnitFacts::Identity& identity : before.identities) + { + if (pairs.count(identity.symbol) || afterNames.count(identity.symbol)) + continue; + if (!identity.nested.empty()) + { + const auto around = pairs.find(identity.enclosing); + if (around != pairs.end()) + wasAtKey["n" + substitute(identity.nested) + '\0' + around->second] + .push_back(identity.symbol); + } + else if (!identity.specialization.empty()) + wasAtKey["s" + substitute(identity.specialization)].push_back( + identity.symbol); + } + for (const auto& [key, sources] : wasAtKey) + { + const auto found = nowAtKey.find(key); + if (found == nowAtKey.end()) + continue; + if (sources.size() == 1 && found->second.size() == 1) + pair(sources.front(), found->second.front()); + else if (missing.empty()) + missing = "'" + llvm::demangle(sources.front()) + + "' has no one-to-one counterpart through its template arguments"; + } + } + } + std::vector out; + for (const auto& [wasSymbol, nowSymbol] : pairs) + { + if (wasSymbol != nowSymbol) + out.push_back({wasSymbol, nowSymbol}); + } + return out; + } + + Judgement const_suggestion::judgeCollisions(const std::vector& units, + std::size_t entity, const std::string& definingFile, + const std::string& function) + { + using llvm::GlobalValue; + const auto indeterminate = [](std::string why) + { return Judgement{std::move(why), Verdict::Indeterminate}; }; + // Strong: external; weak: weak or linkonce, ODR or not. Any other linkage, local above + // all, has no scope across files. + const auto strong = [](GlobalValue::LinkageTypes linkage) + { return GlobalValue::isExternalLinkage(linkage); }; + const auto weak = [](GlobalValue::LinkageTypes linkage) + { return GlobalValue::isWeakLinkage(linkage) || GlobalValue::isLinkOnceLinkage(linkage); }; + // The entity's own linkage, as the defining unit defines it; strong if it shows none. + GlobalValue::LinkageTypes own = GlobalValue::ExternalLinkage; + for (const UnitFacts* unit : units) + { + if (unit->file == definingFile && entity < unit->entities.size() && + unit->entities[entity].linkage) + { + own = *unit->entities[entity].linkage; + } + } + if (!strong(own) && !weak(own)) + return {}; + // A definition first, then a declaration (ruling 1), in the order the units are judged. + for (const UnitFacts* unit : units) + { + if (entity >= unit->entities.size()) + continue; + for (const UnitFacts::Entity::Collision& collision : unit->entities[entity].collisions) + { + if (!collision.linkage || + (!strong(*collision.linkage) && !weak(*collision.linkage))) + continue; + const std::string assumption = collision.symbol + " is also defined in " + + unit->file + "; if " + definingFile + " and " + + unit->file + " are linked together, "; + if (strong(own) && strong(*collision.linkage)) + return indeterminate(assumption + "the two definitions collide"); + if (weak(own) && weak(*collision.linkage)) + { + return indeterminate( + assumption + "the linker keeps one of the two weak definitions, and the " + "check establishes neither which one nor that they are " + "equivalent"); + } + const bool ownIsStrong = strong(own); + return indeterminate(assumption + "the strong definition, in " + + (ownIsStrong ? definingFile : unit->file) + + ", is used and the weak one, in " + + (ownIsStrong ? unit->file : definingFile) + ", is ignored"); + } + } + for (const UnitFacts* unit : units) + { + if (entity >= unit->entities.size()) + continue; + for (const UnitFacts::Entity::Collision& collision : unit->entities[entity].collisions) + { + if (collision.linkage) + continue; + return indeterminate("the rewrite gives " + function + + " the symbol of another declaration (" + + collision.site.expansion.file + ":" + + std::to_string(collision.line) + "); if a file linked with " + + definingFile + " defines it, the two definitions collide"); + } + } + return {}; + } +} // namespace ctrace::stack::analysis diff --git a/src/analysis/ConstSuggestionPlan.cpp b/src/analysis/ConstSuggestionPlan.cpp new file mode 100644 index 0000000..0e18985 --- /dev/null +++ b/src/analysis/ConstSuggestionPlan.cpp @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: Apache-2.0 +// Plans the Clang check of a const suggestion (#217, §5.5): the parameter, the type it must get, +// and the edits that write that type in every declaration the defining file sees. +#include "ConstSuggestionInternal.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ctrace::stack::analysis +{ + namespace + { + using const_suggestion::candidateOf; + using const_suggestion::Edit; + using const_suggestion::MacroDefinitions; + using const_suggestion::realPath; + using const_suggestion::SiteReader; + using const_suggestion::SourceLinkage; + using const_suggestion::SuggestionPlan; + + // Records where each macro definition lies, by file. + class MacroRecorder : public clang::PPCallbacks + { + public: + MacroRecorder(const clang::SourceManager& sm, MacroDefinitions& out) + : sm_(sm), out_(out) + { + } + + void MacroDefined(const clang::Token&, const clang::MacroDirective* directive) override + { + const clang::MacroInfo* info = directive->getMacroInfo(); + const clang::SourceLocation begin = info->getDefinitionLoc(); + const clang::SourceLocation end = info->getDefinitionEndLoc(); + if (!begin.isFileID() || !end.isFileID()) + return; + const auto [file, from] = sm_.getDecomposedLoc(begin); + if (sm_.getFileID(end) == file) + out_[file].push_back({from, sm_.getFileOffset(end)}); + } + + private: + const clang::SourceManager& sm_; + MacroDefinitions& out_; + }; + + // Whether `offset` of `file` lies in a macro definition, as the preprocessor read it: + // continuation lines included, whatever spaces follow their backslash. + bool inMacroDefinition(const MacroDefinitions& macros, clang::FileID file, + std::size_t offset) + { + const auto found = macros.find(file); + return found != macros.end() && + std::any_of(found->second.begin(), found->second.end(), + [&](const std::pair& range) + { return range.first <= offset && offset <= range.second; }); + } + + class Planner + { + public: + Planner(clang::ASTContext& context, const MacroDefinitions& macros, + SuggestionPlan& plan) + : context_(context), sm_(context.getSourceManager()), macros_(macros), plan_(plan) + { + } + + // Writes the suggested type where `param` writes its type. + bool plan(const clang::FunctionDecl& function, const clang::ParmVarDecl& param) + { + param_ = ¶m; + const clang::TypeSourceInfo* written = param.getTypeSourceInfo(); + if (!written) + return fail("the parameter's type is not written in the source"); + clang::TypeLoc loc = written->getTypeLoc(); + for (;;) + { + loc = loc.getUnqualifiedLoc(); + if (auto attributed = loc.getAs()) + loc = attributed.getModifiedLoc(); + else if (auto paren = loc.getAs()) + loc = paren.getInnerLoc(); + else if (auto macro = loc.getAs()) + loc = macro.getInnerLoc(); + else if (auto decayed = loc.getAs()) + loc = decayed.getOriginalLoc(); + else + break; + } + if (auto pointer = loc.getAs()) + return insertConst(function, pointer.getPointeeLoc().getBeginLoc()); + if (auto reference = loc.getAs()) + return insertConst(function, reference.getPointeeLoc().getBeginLoc()); + if (auto reference = loc.getAs()) + { + // The suggestion names the const lvalue reference. + return insertConst(function, reference.getPointeeLoc().getBeginLoc()) && + replace(function, reference.getAmpAmpLoc(), 2, "&"); + } + if (auto array = loc.getAs()) + return insertConst(function, array.getElementLoc().getBeginLoc()); + // A typedef of a pointer: its desugared type, qualified. + clang::TypeLoc named = loc; + if (auto elaborated = loc.getAs()) + named = elaborated.getNamedTypeLoc(); + if (named.getAs() || named.getAs()) + { + const auto* pointer = loc.getType()->getAs(); + if (!pointer || pointer->getPointeeType()->isFunctionType() || + pointer->getPointeeType()->isArrayType()) + { + return fail("the parameter's type is a typedef of no plain pointer"); + } + // The candidate of this declaration's own type, its qualifiers kept (D10). + clang::PrintingPolicy policy(context_.getLangOpts()); + const std::string text = + candidateOf(context_, param.getType()).getAsString(policy); + if (text.find("(unnamed") != std::string::npos || + text.find("(anonymous") != std::string::npos || + text.find("(lambda") != std::string::npos) + { + return fail("the pointed type has no name to write"); + } + const clang::SourceLocation begin = loc.getBeginLoc(); + const clang::SourceLocation end = loc.getEndLoc(); + if (begin.isMacroID() || end.isMacroID()) + return fail("the parameter's typedef is written by a macro"); + // The qualifiers written apart, before or after the name, are folded into + // the candidate: the edit spans them (D10). + const clang::LangOptions& lang = context_.getLangOpts(); + const auto qualifier = [&](const clang::Token& token) + { + if (!token.is(clang::tok::raw_identifier)) + return std::string(); + const llvm::StringRef word = token.getRawIdentifier(); + return word == "const" || word == "volatile" || word == "restrict" + ? word.str() + : std::string(); + }; + std::set folded; + clang::SourceLocation first = begin; + for (clang::SourceLocation at = param.getBeginLoc(); + at.isValid() && !at.isMacroID() && + sm_.isBeforeInTranslationUnit(at, begin);) + { + clang::Token token; + if (clang::Lexer::getRawToken(at, token, sm_, lang, true)) + break; + if (qualifier(token).empty()) + { + folded.clear(); + first = begin; + } + else + { + if (folded.empty()) + first = at; + folded.insert(qualifier(token)); + } + const std::optional next = + clang::Lexer::findNextToken(at, sm_, lang); + at = next ? next->getLocation() : clang::SourceLocation(); + } + clang::SourceLocation last = end; + for (std::optional next = + clang::Lexer::findNextToken(end, sm_, lang); + next && !next->getLocation().isMacroID(); + next = clang::Lexer::findNextToken(next->getLocation(), sm_, lang)) + { + clang::Token token; + if (clang::Lexer::getRawToken(next->getLocation(), token, sm_, lang, + true) || + qualifier(token).empty()) + { + break; + } + folded.insert(qualifier(token)); + last = next->getLocation(); + } + const clang::Qualifiers written = param.getType().getLocalQualifiers(); + if ((written.hasConst() && !folded.count("const")) || + (written.hasVolatile() && !folded.count("volatile")) || + (written.hasRestrict() && !folded.count("restrict"))) + { + return fail( + "the qualifier of the parameter's typedef is written by a macro"); + } + const unsigned length = + sm_.getFileOffset(clang::Lexer::getLocForEndOfToken(last, 0, sm_, lang)) - + sm_.getFileOffset(first); + return replace(function, first, length, text); + } + return fail("the parameter's type has no form this check rewrites"); + } + + private: + bool fail(std::string why) + { + if (!plan_.indeterminate) + plan_.indeterminate = std::move(why); + return false; + } + + bool insertConst(const clang::FunctionDecl& function, clang::SourceLocation at) + { + return replace(function, at, 0, "const "); + } + + bool replace(const clang::FunctionDecl& function, clang::SourceLocation at, + unsigned length, const std::string& text) + { + if (at.isInvalid()) + return fail("the parameter's type has no position"); + bool inArgument = false; + if (at.isMacroID()) + { + if (sm_.isMacroArgExpansion(at)) + { + // Written in a macro argument: rewritten where it is written. + at = sm_.getSpellingLoc(at); + inArgument = true; + } + else + { + // Written by a macro: a macro is text, `const` goes before its + // invocation, unless that invocation writes the declaration too, or + // another parameter, which would take the qualifier. + const clang::CharSourceRange invocation = sm_.getExpansionRange(at); + const auto writes = [&](clang::SourceLocation place) + { + const clang::SourceLocation where = sm_.getExpansionLoc(place); + return !sm_.isBeforeInTranslationUnit(where, invocation.getBegin()) && + !sm_.isBeforeInTranslationUnit(invocation.getEnd(), where); + }; + if (length != 0 || writes(function.getLocation())) + return fail("the declaration is written by a macro definition"); + for (const clang::ParmVarDecl* other : function.parameters()) + { + if (other != param_ && writes(other->getLocation())) + { + return fail("the parameter's type is written by a macro that " + "writes other parameters too"); + } + } + at = invocation.getBegin(); + } + if (at.isMacroID()) + return fail("the declaration is written by a macro definition"); + } + const auto [file, offset] = sm_.getDecomposedLoc(at); + const clang::OptionalFileEntryRef entry = sm_.getFileEntryRefForID(file); + if (!entry) + return fail("the parameter's type is not written in a file"); + if (inMacroDefinition(macros_, file, offset)) + return fail("the declaration is written by a macro definition"); + std::vector& edits = plan_.edits[realPath(entry->getName())]; + const bool seen = std::any_of(edits.begin(), edits.end(), [&](const Edit& edit) + { return edit.offset == offset; }); + if (!seen) + edits.push_back({offset, length, text, sm_.getLineNumber(file, offset)}); + if (inArgument) + inArguments_.push_back(at); + return true; + } + + public: + // The edits written in a macro argument, which other declarations may share. + const std::vector& inArguments() const + { + return inArguments_; + } + + private: + clang::ASTContext& context_; + const clang::SourceManager& sm_; + const MacroDefinitions& macros_; + SuggestionPlan& plan_; + std::vector inArguments_; + const clang::ParmVarDecl* param_ = nullptr; + }; + + // Whether a parameter other than `planned` has its type written at one of `edits`, the + // edits written in a macro argument: a macro argument can write the type of several + // parameters (D5). + class SharedTypes : public clang::RecursiveASTVisitor + { + public: + SharedTypes(const clang::SourceManager& sm, + const std::vector& edits, + const std::set& planned) + : sm_(sm), edits_(edits), planned_(planned) + { + } + + bool VisitParmVarDecl(const clang::ParmVarDecl* parameter) + { + if (!shared && !planned_.count(parameter)) + shared = writtenAtAnEdit(parameter->getTypeSourceInfo()); + return true; + } + + bool shared = false; + std::uint8_t reservedPadding_[7] = {}; + + private: + // Whether a part of the type, written in a macro argument, is written at an edit. + bool writtenAtAnEdit(const clang::TypeSourceInfo* written) const + { + if (!written) + return false; + for (clang::TypeLoc loc = written->getTypeLoc(); !loc.isNull(); + loc = loc.getNextTypeLoc()) + { + const clang::SourceLocation at = loc.getBeginLoc(); + if (at.isMacroID() && sm_.isMacroArgExpansion(at) && + std::find(edits_.begin(), edits_.end(), sm_.getSpellingLoc(at)) != + edits_.end()) + { + return true; + } + } + return false; + } + + const clang::SourceManager& sm_; + const std::vector& edits_; + const std::set& planned_; + }; + + // A type as a declaration of `name` writes it. + std::string declaring(const clang::ASTContext& context, clang::QualType type, + llvm::StringRef name) + { + clang::PrintingPolicy policy(context.getLangOpts()); + policy.AnonymousTagLocations = false; + std::string out; + llvm::raw_string_ostream text(out); + type.print(text, policy, name); + return out; + } + + // The candidate as a diagnostic displays it: as Clang writes it, struct, union and enum + // included where C needs them, except that a type without a name is , + // as the rule's own lines write it (stage 2, ruling 3). + std::string shownOf(const clang::ASTContext& context, clang::QualType type, + llvm::StringRef name) + { + std::string out = declaring(context, type, name); + const clang::Type* base = type.getTypePtr(); + while (!base->getPointeeType().isNull()) + base = base->getPointeeType().getTypePtr(); + const clang::TagDecl* tag = base->getAsTagDecl(); + if (tag && !tag->getIdentifier() && !tag->getTypedefNameForAnonDecl()) + { + clang::PrintingPolicy policy(context.getLangOpts()); + policy.AnonymousTagLocations = false; + const std::string spelled = + clang::QualType(base, 0).getUnqualifiedType().getAsString(policy); + if (const std::size_t at = out.find(spelled); at != std::string::npos) + out.replace(at, spelled.size(), ""); + } + return out; + } + + // What the candidate stands for, as the trace names it (Texts). + const char* alternativeOf(clang::QualType type) + { + if (type->isRValueReferenceType()) + return "reference to const; passing by value is not checked"; + if (type->isReferenceType()) + return "reference to const"; + return type.isConstQualified() ? "pointer to const, top-level const kept" + : "pointer to const"; + } + } // namespace + + // The type the parameter has once rewritten, which the edits write (§3): a pointer to const, + // its top-level qualifiers kept; a reference to const, for an rvalue reference too. Null for + // another type. + clang::QualType const_suggestion::candidateOf(clang::ASTContext& context, clang::QualType type) + { + if (const auto* pointer = type->getAs()) + { + return context.getQualifiedType( + context.getPointerType(pointer->getPointeeType().withConst()), + type.getQualifiers()); + } + if (type->isReferenceType()) + return context.getLValueReferenceType(type.getNonReferenceType().withConst()); + return {}; + } + + // A type written canonically. An anonymous type, such as a lambda's, is written without + // its location: the path of that location depends on how the file was named, relative + // before the rewrite and absolute once the rewritten file is remapped. + std::string const_suggestion::writtenType(const clang::ASTContext& context, + clang::QualType type) + { + clang::PrintingPolicy policy(context.getLangOpts()); + policy.AnonymousTagLocations = false; + return type.getCanonicalType().getAsString(policy); + } + + SourceLinkage const_suggestion::linkageOf(const clang::FunctionDecl& function) + { + switch (function.getFormalLinkage()) + { + case clang::Linkage::External: + case clang::Linkage::Module: + return SourceLinkage::External; + case clang::Linkage::Internal: + case clang::Linkage::UniqueExternal: + return SourceLinkage::Internal; + default: + return SourceLinkage::None; + } + } + + std::unique_ptr + const_suggestion::recordMacroDefinitions(const clang::SourceManager& sm, MacroDefinitions& out) + { + return std::make_unique(sm, out); + } + + SuggestionPlan const_suggestion::plan(clang::ASTContext& context, + const MacroDefinitions& macros, + const clang::FunctionDecl& function, + std::optional sourceIndex) + { + SuggestionPlan out; + const auto* method = llvm::dyn_cast(&function); + out.declarableElsewhere = method == nullptr && function.isExternallyVisible(); + const clang::FunctionDecl* instantiated = function.getTemplateInstantiationPattern(); + const clang::FunctionDecl* pattern = instantiated ? instantiated : &function; + // The source identity (§3): the declarations the plan rewrites, by both positions of + // their names, and a specialization's pattern, by its first declaration. + SiteReader sites(context.getSourceManager(), nullptr); + for (const clang::FunctionDecl* declaration : pattern->redecls()) + { + if (!declaration->isImplicit()) + out.entity.redeclarations.push_back(sites(declaration->getLocation())); + } + std::sort(out.entity.redeclarations.begin(), out.entity.redeclarations.end()); + out.entity.name = function.getDeclName().getAsString(); + const clang::DeclarationName::NameKind kind = function.getDeclName().getNameKind(); + out.entity.operatorName = kind == clang::DeclarationName::CXXOperatorName || + kind == clang::DeclarationName::CXXLiteralOperatorName; + out.entity.linkage = linkageOf(function); + if (instantiated) + out.entity.templateOrigin = sites(instantiated->getFirstDecl()->getLocation()); + if (!sourceIndex) + { + out.indeterminate = "the parameter is not found in the source"; + return out; + } + out.sourceIndex = *sourceIndex; + if (out.sourceIndex >= function.getNumParams()) + { + out.indeterminate = "the parameter is not found in the source"; + return out; + } + const clang::ParmVarDecl& param = *function.getParamDecl(out.sourceIndex); + const clang::QualType candidate = candidateOf(context, param.getType()); + if (candidate.isNull()) + { + out.indeterminate = "the parameter is no pointer or reference"; + return out; + } + out.candidate = {declaring(context, param.getType(), param.getName()), + declaring(context, candidate, param.getName()), + alternativeOf(param.getType()), + shownOf(context, candidate, param.getName())}; + Planner planner(context, macros, out); + for (const clang::FunctionDecl* declaration : pattern->redecls()) + { + if (declaration->isImplicit()) + continue; + if (out.sourceIndex >= declaration->getNumParams()) + { + out.indeterminate = "the declarations disagree on the parameters"; + return out; + } + if (!planner.plan(*declaration, *declaration->getParamDecl(out.sourceIndex))) + return out; + out.parameters.push_back( + sites(declaration->getParamDecl(out.sourceIndex)->getLocation())); + } + std::sort(out.parameters.begin(), out.parameters.end()); + // The plan's scope (§5.5): an edit written in a macro argument serves only the parameter + // planned. + if (!planner.inArguments().empty()) + { + std::set planned; + for (const clang::FunctionDecl* declaration : pattern->redecls()) + { + if (out.sourceIndex < declaration->getNumParams()) + planned.insert(declaration->getParamDecl(out.sourceIndex)); + } + SharedTypes shared(context.getSourceManager(), planner.inArguments(), planned); + shared.TraverseDecl(context.getTranslationUnitDecl()); + if (shared.shared) + { + out.indeterminate = "the parameter's type is written in a macro argument that " + "writes other parameters too"; + } + } + return out; + } +} // namespace ctrace::stack::analysis diff --git a/src/analysis/InputPipeline.cpp b/src/analysis/InputPipeline.cpp index 0828f54..78802c7 100644 --- a/src/analysis/InputPipeline.cpp +++ b/src/analysis/InputPipeline.cpp @@ -843,6 +843,29 @@ namespace ctrace::stack::analysis return detectFromExtension(path); } + bool buildSourceCompileCommand(const std::string& filename, LanguageType language, + const AnalysisConfig& config, std::vector& args, + std::string& workingDir, std::string& error) + { + std::error_code cwdErr; + const std::filesystem::path baseDir = config.inputBaseDir.empty() + ? std::filesystem::current_path(cwdErr) + : std::filesystem::path(config.inputBaseDir); + const std::string inputPath = makeAbsolutePathFrom(filename, baseDir.string()); + return buildCompileArgs(filename, inputPath, language, config, args, workingDir, error); + } + + bool runSerializedCompile(const std::string& workingDir, const std::function& compile, + std::string& error) + { + std::lock_guard compileLock(gCompileInvokeMutex); + ScopedCurrentPath cwdGuard(workingDir, error); + if (!error.empty()) + return false; + compile(); + return true; + } + ModuleLoadResult loadModuleForAnalysis(const std::string& filename, const AnalysisConfig& config, llvm::LLVMContext& ctx, llvm::SMDiagnostic& err) diff --git a/src/analyzer/AnalysisPipeline.cpp b/src/analyzer/AnalysisPipeline.cpp index 515e368..bd6827f 100644 --- a/src/analyzer/AnalysisPipeline.cpp +++ b/src/analyzer/AnalysisPipeline.cpp @@ -12,6 +12,7 @@ #include "analysis/AllocaUsage.hpp" #include "analysis/ConstParamAnalysis.hpp" +#include "analysis/ConstSuggestionCheck.hpp" #include "analysis/CommandInjectionAnalysis.hpp" #include "analysis/DuplicateIfCondition.hpp" #include "analysis/DynamicAlloca.hpp" @@ -647,9 +648,10 @@ namespace ctrace::stack::analyzer auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; const auto* otherModules = state.config.constPointeeParamIndex.get(); - const std::vector issues = + std::vector issues = analysis::analyzeConstParams(state.mod, shouldAnalyze, otherModules); + analysis::checkConstSuggestions(state.mod, issues, state.config); appendConstParamDiagnostics(state.result, issues); }, kPrepared, kNone, true, ExecutionModel::SubscriberCompatible}); diff --git a/src/analyzer/DiagnosticEmitter.cpp b/src/analyzer/DiagnosticEmitter.cpp index 8e37f32..b9be9fa 100644 --- a/src/analyzer/DiagnosticEmitter.cpp +++ b/src/analyzer/DiagnosticEmitter.cpp @@ -1051,8 +1051,13 @@ namespace ctrace::stack::analyzer if (issue.isRvalueRef) { - body << kDiagIndentArrow << "consider passing by value (" << issue.suggestedType - << ") or const reference (" << issue.suggestedTypeAlt << ")\n"; + // Without an alternative, the check names the const reference it tried (D12). + if (issue.suggestedTypeAlt.empty()) + body << kDiagIndentArrow << "consider passing by const reference (" + << issue.suggestedType << ")\n"; + else + body << kDiagIndentArrow << "consider passing by value (" << issue.suggestedType + << ") or const reference (" << issue.suggestedTypeAlt << ")\n"; body << kDiagIndentArrow << "current type: " << issue.currentType << "\n"; } else if (issue.pointerConstOnly) @@ -1071,6 +1076,8 @@ namespace ctrace::stack::analyzer body << kDiagIndentArrow << "the callee's body is not checked: it may still modify " << "the " << object << " object through a cast\n"; } + for (const std::string& line : issue.checkLines) + body << kDiagIndentArrow << line << "\n"; DiagnosticBuilder builder; builder.function(issue.funcName) diff --git a/src/app/AnalyzerApp.cpp b/src/app/AnalyzerApp.cpp index 5a1cde3..71745ca 100644 --- a/src/app/AnalyzerApp.cpp +++ b/src/app/AnalyzerApp.cpp @@ -47,6 +47,7 @@ #include "analysis/AnalyzerUtils.hpp" #include "analysis/CompileCommands.hpp" #include "analysis/ConstParamAnalysis.hpp" +#include "analysis/ConstSuggestionCheck.hpp" #include "analysis/DuplicateIfCondition.hpp" #include "analysis/FunctionFilter.hpp" #include "analysis/GlobalReadBeforeWriteAnalysis.hpp" @@ -988,6 +989,17 @@ static AppStatus analyzeWithSharedModuleLoading(const std::vector& } loadedModules.swap(orderedLoadedModules); + { + // Every input analyzed together, for the Clang check of const suggestions (#217). + auto units = std::make_shared(); + for (const auto& loaded : loadedModules) + { + units->units.push_back({loaded.filename, loaded.module.get(), + analysis::detectFromExtension(loaded.filename)}); + } + cfg.analyzedUnits = std::move(units); + } + { // A function found never to return ends the paths that call it in the other modules; // repeat until no round finds a new one, for chains across several files. A caller may @@ -1161,6 +1173,8 @@ static AppStatus analyzeWithSharedModuleLoading(const std::vector& logText(coretrace::Level::Info, emptyMsg); results.emplace_back(loaded.filename, std::move(result)); } + // The modules are freed on return. + cfg.analyzedUnits.reset(); return AppStatus::success(); } @@ -1203,7 +1217,9 @@ static AppStatus analyzeWithoutSharedModuleLoading(const std::vector kCandidates = { + static constexpr std::array kCandidates = { {{"-h", "-h"}, {"--help", "--help"}, {"--demangle", "--demangle"}, @@ -89,6 +89,7 @@ namespace ctrace::stack::cli {"--no-uninitialized-cross-tu", "--no-uninitialized-cross-tu"}, {"--resource-summary-cache-dir", "--resource-summary-cache-dir"}, {"--resource-summary-cache-memory-only", "--resource-summary-cache-memory-only"}, + {"--experimental-const-suggestion-check", "--experimental-const-suggestion-check"}, {"--compile-ir-cache-dir", "--compile-ir-cache-dir"}, {"--compile-ir-format", "--compile-ir-format"}, {"--compile-ir-format=bc", "--compile-ir-format=bc"}, @@ -822,12 +823,17 @@ namespace ctrace::stack::cli cfg.resourceSummaryMemoryOnly = value; } + void setConfigConstSuggestionCheck(AnalysisConfig& cfg, bool value) + { + cfg.constSuggestionCheck = value; + } + void setParsedIncludeCompdbDeps(ParsedArguments& parsed, bool value) { parsed.includeCompdbDeps = value; } - constexpr std::array, 7> kConfigBoolSpecs = {{ + constexpr std::array, 8> kConfigBoolSpecs = {{ {"timing", &setConfigTiming}, {"warnings-only", &setConfigWarningsOnly}, {"quiet", &setConfigQuiet}, @@ -835,6 +841,7 @@ namespace ctrace::stack::cli {"resource-cross-tu", &setConfigResourceCrossTU}, {"uninitialized-cross-tu", &setConfigUninitializedCrossTU}, {"resource-summary-cache-memory-only", &setConfigResourceSummaryMemoryOnly}, + {"experimental-const-suggestion-check", &setConfigConstSuggestionCheck}, }}; constexpr std::array, 1> kParsedBoolSpecs = {{ @@ -1560,6 +1567,11 @@ namespace ctrace::stack::cli cfg.resourceSummaryMemoryOnly = true; continue; } + if (argStr == "--experimental-const-suggestion-check") + { + cfg.constSuggestionCheck = true; + continue; + } if (argStr == "--compile-commands" || argStr == "--compdb") { if (i + 1 >= argc) diff --git a/src/mangle.cpp b/src/mangle.cpp index bed2557..459ae7c 100644 --- a/src/mangle.cpp +++ b/src/mangle.cpp @@ -1,8 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 #include "mangle.hpp" +#include + namespace ctrace_tools { + namespace + { + // A symbol, demangled by the C++ runtime's demangler, or by LLVM's when the runtime's + // predates the symbol's mangling (#240); empty when neither reads it. + std::string demangled(const std::string& name) + { + int status = 0; + std::unique_ptr out( + abi::__cxa_demangle(name.c_str(), nullptr, nullptr, &status), std::free); + if (status != 0 || !out) + out.reset(llvm::itaniumDemangle(name)); + return out ? out.get() : ""; + } + } // namespace + + bool isMangled(std::string_view name) noexcept + { + return name.starts_with("_Z") && !demangled(std::string(name)).empty(); + } std::string mangleFunction(const std::string& namespaceName, const std::string& functionName, const std::vector& paramTypes) @@ -71,14 +92,8 @@ namespace ctrace_tools std::string demangle(const char* name) { - int status = 0; - char* demangled = abi::__cxa_demangle(name, nullptr, nullptr, &status); - - std::string result = (status == 0 && demangled) ? demangled : name; - - free(demangled); - - return result; + std::string result = demangled(name); + return result.empty() ? name : result; } std::string canonicalizeMangledName(std::string_view name) diff --git a/test/pointer_reference-const_correctness/const-suggestion-abi-none.cpp b/test/pointer_reference-const_correctness/const-suggestion-abi-none.cpp new file mode 100644 index 0000000..d58272c --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-abi-none.cpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a function whose symbol does not change gets no ABI note: internal linkage, or a C name. + +// strict-expectation-details: true +// not contains: ↳ ABI: + +// at line 12, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'internal_read(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +static int internal_read(int* p) +{ + return *p; +} + +namespace +{ + // at line 24, column 0 + // [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function '(anonymous namespace)::anonymous_read(int*)' is never used to modify the pointed object + // ↳ current type: int *p + // ↳ suggested type: const int *p + // ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + int anonymous_read(int* p) + { + return *p; + } +} // namespace + +// at line 35, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'c_read' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +extern "C" int c_read(int* p) +{ + return *p; +} + +int use_internal_reads() +{ + int value = 1; + return internal_read(&value) + anonymous_read(&value); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-abi.cpp b/test/pointer_reference-const_correctness/const-suggestion-abi.cpp new file mode 100644 index 0000000..aae8824 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-abi.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: in C++, const on the pointee changes the symbol of a function with external linkage. +// Code compiled separately against the current declaration no longer links: the message says so. + +// strict-expectation-details: true + +// at line 13, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'external_read(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_Z13external_readPi -> _Z13external_readPKi): code compiled separately must use the updated declaration and be recompiled +int external_read(int* p) +{ + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-deduction-kept.cpp b/test/pointer_reference-const_correctness/const-suggestion-deduction-kept.cpp new file mode 100644 index 0000000..f1dbb8a --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-deduction-kept.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: rewriting a forwarding reference as a const lvalue reference changes what an lvalue +// argument deduces: F is const G& before the rewrite and G after it. That specialization, and the +// lambda instantiated inside it, change their names, but not their code: they are paired, and both +// suggestions are kept. + +// strict-expectation-details: true + +// at line 24, column 0 +// [ !Info! ] ConstParameterNotModified.ReferenceRvaluePreferValue: parameter 'f' in function 'int apply_twice(from_temporaries()::$_0&&)' is an rvalue reference and is never used to modify the referred object +// ↳ consider passing by const reference (const &f) +// ↳ current type: &&f +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested const reference, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside; passing by value is not checked + +// at line 24, column 0 +// [ !Info! ] ConstParameterNotModified.ReferenceRvaluePreferValue: parameter 'f' in function 'int apply_twice(from_temporaries()::$_1&&)' is an rvalue reference and is never used to modify the referred object +// ↳ consider passing by const reference (const &f) +// ↳ current type: &&f +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested const reference, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside; passing by value is not checked + +// The lvalue passes a const reference already: no suggestion for that specialization. +// not contains: in function 'int apply_twice int apply_twice(F&& f) +{ + const auto twice = [&](int x) { return f(f(x)); }; + return twice(1); +} + +int from_temporaries() +{ + return apply_twice([](int x) { return x & 1; }) | apply_twice([](int x) { return x | 2; }); +} + +int from_lvalue() +{ + const auto g = [](int x) { return x ^ 4; }; + return apply_twice(g); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-deduction-refused.cpp b/test/pointer_reference-const_correctness/const-suggestion-deduction-refused.cpp new file mode 100644 index 0000000..be3700a --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-deduction-refused.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: specializations of the rewritten template whose deduced type changes are paired only when +// the correspondence is one to one, and what they select is still compared. +// - call: g passes G& and h passes const G&; after the rewrite both deduce G, so two +// specializations become one. Without a one-to-one correspondence, nothing shows that the +// rewrite keeps or changes what they do: both suggestions are kept, not checked, and the reason +// names the specializations. The lambdas are noexcept, so that all three specializations get +// the same attributes: only the correspondence tells them apart. +// - pick: the lvalue's specialization is paired, and the condition of its `if constexpr` changes +// with what F deduces: the suggestion is removed. + +// strict-expectation-details: true +// not contains: in function 'int pick< + +// at line 28, column 0 +// [ !Info! ] ConstParameterNotModified.ReferenceRvaluePreferValue: parameter 'f' in function 'int call(from_temporary()::$_0&&)' is an rvalue reference and is only read, or passed where a reference to const is expected +// ↳ consider passing by const reference (const &f) +// ↳ current type: &&f +// ↳ the callee's body is not checked: it may still modify the referred object through a cast +// ↳ not checked with Clang: 'int call(from_lvalues()::$_0&)' and 'int call(from_lvalues()::$_0 const&)' both become 'int call(from_lvalues()::$_0 const&)': their correspondence is not one to one + +// at line 28, column 0 +// [ !Info! ] ConstParameterNotModified.Reference: parameter 'f' in function 'int call(from_lvalues()::$_0&)' is only read, or passed where a reference to const is expected +// ↳ current type: &f +// ↳ suggested type: const &f +// ↳ the callee's body is not checked: it may still modify the referred object through a cast +// ↳ not checked with Clang: 'int call(from_lvalues()::$_0&)' and 'int call(from_lvalues()::$_0 const&)' both become 'int call(from_lvalues()::$_0 const&)': their correspondence is not one to one +template int call(F&& f) +{ + return f(1); +} + +int from_temporary() +{ + return call([](int x) noexcept { return x & 1; }); +} + +int from_lvalues() +{ + auto g = [](int x) noexcept { return x | 2; }; + const auto& h = g; + return call(g) | call(h); +} + +template struct is_reference +{ + static constexpr bool value = false; +}; + +template struct is_reference +{ + static constexpr bool value = true; +}; + +template int pick(F&& f) +{ + if constexpr (is_reference::value) + return f(4); + else + return f(8); +} + +int pick_temporary() +{ + return pick([](int x) { return x & 3; }); +} + +int pick_lvalue() +{ + const auto g = [](int x) { return x ^ 5; }; + return pick(g); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-explicit-object.cpp b/test/pointer_reference-const_correctness/const-suggestion-explicit-object.cpp new file mode 100644 index 0000000..7a94a02 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-explicit-object.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: an explicit object parameter takes the first place in the parameters' debug numbering, as +// an implicit `this` would not: the check must rewrite p, not self. read's address is stored with +// the old type, so read's suggested type does not compile and is removed; peek's is checked. + +// analyzer-arg: --experimental-const-suggestion-check +// analyzer-arg: --compile-arg=-std=c++23 +// not contains: in function 'Reader::read( +// strict-expectation-details: true + +// at line 22, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'Reader::peek(this Reader const&, int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +struct Reader +{ + int read(this const Reader& self, int* p) + { + return *p; + } + int peek(this const Reader& self, int* p) + { + return *p; + } +}; + +int (*stored)(const Reader&, int*) = &Reader::read; + +int use_reader() +{ + Reader reader; + int value = 1; + return reader.read(&value) == reader.peek(&value); +} diff --git a/test/pointer_reference-const_correctness/const-suggestion-facts-without-code-change.cpp b/test/pointer_reference-const_correctness/const-suggestion-facts-without-code-change.cpp new file mode 100644 index 0000000..08fab07 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-facts-without-code-change.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a call or a constant condition that the suggested type changes refutes the suggestion, +// even when the generated code stays the same: +// - sized: sizeof names sized(bool) before the rewrite and sized(const int*) after it; both return +// int, so the size, and the code, stay the same; +// - typed: if constexpr tests the parameter's own type, and both branches are the same code. + +// not contains: parameter 'p' in function 'sized(int*)' +// not contains: parameter 'p' in function 'typed(int*)' +#include + +int sized(int* p) +{ + return *p; +} + +int sized(bool flag); + +int size_of_call(const int* q) +{ + return static_cast(sizeof(sized(q))); +} + +int typed(int* p) +{ + if constexpr (std::is_const_v>) + return *p; + else + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-incomplete-inspection.cpp b/test/pointer_reference-const_correctness/const-suggestion-incomplete-inspection.cpp new file mode 100644 index 0000000..c25c28b --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-incomplete-inspection.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: the suggested type can make a compilation stop at a fatal error, after which the code it +// would have parsed is not inspected. Here sizeof(sized(p)) selects sized(int*) before the rewrite, +// and the template once p points to const: deducing its return type instantiates Deep without end. +// What the rewrite changes is then unknown: the suggestion is kept, not checked. + +// strict-expectation-details: true + +template struct Deep +{ + static constexpr int value = Deep::value + 1; +}; + +int sized(int* p); + +template auto sized(const T* p) +{ + return Deep::value + *p; +} + +// at line 26, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_sized(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ not checked with Clang: the rewritten code cannot be inspected entirely, a fatal error stops its compilation: +int read_sized(int* p) +{ + return *p + static_cast(sizeof(sized(p))); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-inheriting-constructor.cpp b/test/pointer_reference-const_correctness/const-suggestion-inheriting-constructor.cpp new file mode 100644 index 0000000..561b06e --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-inheriting-constructor.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a class that inherits a constructor (`using Base::Base`) gets constructors of its own, +// whose symbols name the inherited constructor's parameter types: the rewrite renames them too. +// The check pairs them before and after the rewrite through the constructor they inherit, so +// each suggestion is checked: a plain class, a class template as the base, a class template as +// the derived class, and a constructor template, inherited twice, for int* and for double*: the +// derived class inherits two constructors, one from each specialization. + +// strict-expectation-details: true + +// at line 19, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'Base::Base(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +struct Base +{ + int v; + Base(int* p) : v(*p) {} +}; + +struct Derived : Base +{ + using Base::Base; + int w = 0; +}; + +int make_derived(int x) +{ + Derived d(&x); + return d.v; +} + +// at line 42, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'TemplateBase::TemplateBase(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +template struct TemplateBase +{ + T v; + TemplateBase(T* p) : v(*p) {} +}; + +struct FromTemplate : TemplateBase +{ + using TemplateBase::TemplateBase; + int w = 0; +}; + +int make_from_template(int x) +{ + FromTemplate d(&x); + return d.v; +} + +// at line 65, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'PlainBase::PlainBase(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +struct PlainBase +{ + int v; + PlainBase(int* p) : v(*p) {} +}; + +template struct TemplateDerived : PlainBase +{ + using PlainBase::PlainBase; + T w = 0; +}; + +int make_template_derived(int x) +{ + TemplateDerived d(&x); + return d.v; +} + +// at line 94, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'GenericBase::GenericBase(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// at line 94, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'GenericBase::GenericBase(double*)' is never used to modify the pointed object +// ↳ current type: double *p +// ↳ suggested type: const double *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +struct GenericBase +{ + int v; + template GenericBase(T* p) : v(*p) {} +}; + +struct FromGeneric : GenericBase +{ + using GenericBase::GenericBase; + int w = 0; +}; + +int make_from_generic(int x) +{ + FromGeneric d(&x); + return d.v; +} + +int make_from_generic_double(double x) +{ + FromGeneric d(&x); + return d.v; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-invalid-declaration.c b/test/pointer_reference-const_correctness/const-suggestion-invalid-declaration.c new file mode 100644 index 0000000..e48c93e --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-invalid-declaration.c @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: one macro invocation declares the function and a variable whose type its argument also +// writes. The rewrite changes the variable's type, which then contradicts the variable's other +// declaration: the variable becomes invalid, and the file no longer compiles. That error comes +// from a declaration outside the plan: it does not refute the suggestion, which is kept, not +// checked. The expectation stops before the file and the line. + +// strict-expectation-details: true + +extern int* read_value_slot; + +#define PAIR(name, type) \ + int name(type p); \ + type name##_slot; + +PAIR(read_value, int*) + +// at line 23, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_value' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ not checked with Clang: the rewrite also changes the type of read_value_slot ( +int read_value(int* p) +{ + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-ir-input.ll b/test/pointer_reference-const_correctness/const-suggestion-ir-input.ll new file mode 100644 index 0000000..26f7f23 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-ir-input.ll @@ -0,0 +1,43 @@ +; ModuleID = 'const-suggestion-ir-input.c' +source_filename = "const-suggestion-ir-input.c" +target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "arm64-apple-macosx15.0.0" + +; Function Attrs: noinline nounwind optnone ssp uwtable(sync) +define i32 @read_value_ir(ptr noundef %p) #0 !dbg !10 { +entry: + %p.addr = alloca ptr, align 8 + store ptr %p, ptr %p.addr, align 8 + #dbg_declare(ptr %p.addr, !16, !DIExpression(), !17) + %0 = load ptr, ptr %p.addr, align 8, !dbg !18 + %1 = load i32, ptr %0, align 4, !dbg !19 + ret i32 %1, !dbg !20 +} + +attributes #0 = { noinline nounwind optnone ssp uwtable(sync) "frame-pointer"="non-leaf" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="apple-m1" "target-features"="+aes,+altnzcv,+ccdp,+ccidx,+ccpp,+complxnum,+crc,+dit,+dotprod,+flagm,+fp-armv8,+fp16fml,+fptoint,+fullfp16,+jsconv,+lse,+neon,+pauth,+perfmon,+predres,+ras,+rcpc,+rdm,+sb,+sha2,+sha3,+specrestrict,+ssbs,+v8.1a,+v8.2a,+v8.3a,+v8.4a,+v8a,+zcm,+zcz" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6} +!llvm.dbg.cu = !{!7} +!llvm.ident = !{!9} + +!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 15, i32 5]} +!1 = !{i32 7, !"Dwarf Version", i32 5} +!2 = !{i32 2, !"Debug Info Version", i32 3} +!3 = !{i32 1, !"wchar_size", i32 4} +!4 = !{i32 8, !"PIC Level", i32 2} +!5 = !{i32 7, !"uwtable", i32 1} +!6 = !{i32 7, !"frame-pointer", i32 1} +!7 = distinct !DICompileUnit(language: DW_LANG_C11, file: !8, producer: "Homebrew clang version 20.1.2", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: Apple, sysroot: "/Library/Developer/CommandLineTools/SDKs/MacOSX15.sdk", sdk: "MacOSX15.sdk") +!8 = !DIFile(filename: "const-suggestion-ir-input.c", directory: ".", checksumkind: CSK_MD5, checksum: "929812bcf8a8ea56e7e27bdc384c05af") +!9 = !{!"Homebrew clang version 20.1.2"} +!10 = distinct !DISubprogram(name: "read_value_ir", scope: !8, file: !8, line: 1, type: !11, scopeLine: 2, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !7, retainedNodes: !15) +!11 = !DISubroutineType(types: !12) +!12 = !{!13, !14} +!13 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed) +!14 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !13, size: 64) +!15 = !{} +!16 = !DILocalVariable(name: "p", arg: 1, scope: !10, file: !8, line: 1, type: !14) +!17 = !DILocation(line: 1, column: 24, scope: !10) +!18 = !DILocation(line: 3, column: 13, scope: !10) +!19 = !DILocation(line: 3, column: 12, scope: !10) +!20 = !DILocation(line: 3, column: 5, scope: !10) diff --git a/test/pointer_reference-const_correctness/const-suggestion-kept-blocks.cpp b/test/pointer_reference-const_correctness/const-suggestion-kept-blocks.cpp new file mode 100644 index 0000000..9200c23 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-kept-blocks.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: the suggestion is kept when the generated code is the same. log_text's join block has +// three predecessors, which the printed IR lists in a comment, in an order that copying a module +// may change: comments are not code. + +// strict-expectation-details: true +#include + +void sink(const std::string& text); + +static void log_text(const std::string& text) +{ + if (text.empty()) + return; + if (text.back() == '\n') + { + sink(text); + } + else + { + sink(text + "\n"); + } +} + +// at line 31, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_and_log(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_Z12read_and_logPi -> _Z12read_and_logPKi): code compiled separately must use the updated declaration and be recompiled +int read_and_log(int* p) +{ + log_text("value"); + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-kept-inlined.cpp b/test/pointer_reference-const_correctness/const-suggestion-kept-inlined.cpp new file mode 100644 index 0000000..f454298 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-kept-inlined.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: the suggestion is kept when the generated code is the same. Inlining read_once, which is +// instantiated with a lambda declared in the function, names a block after it: the name holds the +// function's symbol, which the rewrite changes. Names of values and blocks are not code. + +// strict-expectation-details: true +template __attribute__((always_inline)) inline int read_once(Read read, bool skip) +{ + if (skip) + return 0; + return read(); +} + +// at line 20, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_with_lambda(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_Z16read_with_lambdaPi -> _Z16read_with_lambdaPKi): code compiled separately must use the updated declaration and be recompiled +int read_with_lambda(int* p) +{ + auto get = [p] { return *p; }; + return read_once(get, false); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-kept.c b/test/pointer_reference-const_correctness/const-suggestion-kept.c new file mode 100644 index 0000000..8688246 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-kept.c @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: the suggestion is kept when the generated code is the same. Inlining a callee with a +// restrict parameter adds metadata whose numbers move with the debug information of the new type. + +// strict-expectation-details: true +// not contains: ↳ ABI: + +static inline __attribute__((always_inline)) int load(const int* __restrict q) +{ + return *q; +} + +// at line 19, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_restricted' is only read, or passed where a pointer to const is expected +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ the callee's body is not checked: it may still modify the pointed object through a cast +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int read_restricted(int* p) +{ + return load(p); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-kept.cpp b/test/pointer_reference-const_correctness/const-suggestion-kept.cpp new file mode 100644 index 0000000..ee98ca6 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-kept.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: the suggestion is kept when the generated code is the same once the symbols the rewrite +// renames are paired. The typeinfo name of a local type holds the function's mangled name, which +// the rewrite changes with it: typeid shows it at run time, so the message says so. + +// strict-expectation-details: true + +// at line 15, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_or_throw(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside, except for the differences below +// ↳ difference: the name typeid gives a type named after the function changes, from "Z13read_or_throwPiE7Missing" to "Z13read_or_throwPKiE7Missing" +// ↳ ABI: the suggested type changes the symbol (_Z13read_or_throwPi -> _Z13read_or_throwPKi): code compiled separately must use the updated declaration and be recompiled +int read_or_throw(int* p) +{ + struct Missing + { + }; + if (!p) + throw Missing{}; + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-macro-params.c b/test/pointer_reference-const_correctness/const-suggestion-macro-params.c new file mode 100644 index 0000000..3f163c2 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-macro-params.c @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a macro that writes several parameters cannot take `const` before its invocation: the +// qualifier would go to the first parameter it writes, n, not to q. The suggestion is kept as not +// checked. + +// strict-expectation-details: true + +// at line 17, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'q' in function 'read_both' is never used to modify the pointed object +// ↳ current type: int *q +// ↳ suggested type: const int *q +// ↳ not checked with Clang: the parameter's type is written by a macro that writes other parameters too + +// analyzer-arg: --experimental-const-suggestion-check +#define READ_ARGS int n, int *q + +int read_both(READ_ARGS) +{ + n = (*q != 0); + return n; +} diff --git a/test/pointer_reference-const_correctness/const-suggestion-macro-shared-type.c b/test/pointer_reference-const_correctness/const-suggestion-macro-shared-type.c new file mode 100644 index 0000000..47b51ff --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-macro-shared-type.c @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a macro argument can write the type of several parameters. Rewriting the type of `src` +// where it is written would rewrite `dst` too, which copy_one writes through: the rewrite would +// reach beyond the suggestion. The suggestion is kept, not checked. + +// strict-expectation-details: true + +#define DECLARE_COPY(name, T) int name(T src, T dst) + +// at line 15, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'src' in function 'copy_one' is never used to modify the pointed object +// ↳ current type: int *src +// ↳ suggested type: const int *src +// ↳ not checked with Clang: the parameter's type is written in a macro argument that writes other parameters too +DECLARE_COPY(copy_one, int*) +{ + *dst = *src; + return 0; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-macro-two-functions.c b/test/pointer_reference-const_correctness/const-suggestion-macro-two-functions.c new file mode 100644 index 0000000..1eeeaf3 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-macro-two-functions.c @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: one macro invocation can declare several functions, whose names it expands at the same +// place. Each one is also known by where its name is written: the check rewrites the function the +// suggestion names and leaves the other one, which writes through its parameter, whichever the +// invocation declares first. + +// strict-expectation-details: true + +#define DECLARE_TWO(a, Ta, b, Tb) \ + int a(Ta p); \ + int b(Tb q); + +DECLARE_TWO(read_first, int*, write_first, int*) +DECLARE_TWO(write_second, int*, read_second, int*) + +// at line 21, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_first' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int read_first(int* p) +{ + return *p; +} + +int write_first(int* q) +{ + *q = 1; + return 0; +} + +int write_second(int* p) +{ + *p = 2; + return 0; +} + +// at line 43, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'q' in function 'read_second' is never used to modify the pointed object +// ↳ current type: int *q +// ↳ suggested type: const int *q +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int read_second(int* q) +{ + return *q; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-macros.c b/test/pointer_reference-const_correctness/const-suggestion-macros.c new file mode 100644 index 0000000..3c10254 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-macros.c @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: types written by macros. A macro argument is rewritten where it is written; a macro that +// writes only the parameter gets const before its invocation, since a macro is text. A macro +// definition that writes the declaration is not rewritten: the suggestion is kept, not checked. + +// strict-expectation-details: true +// not contains: parameter 'buffer' in function 'object_macro_callback' +// not contains: ↳ ABI: +#define BYTE unsigned char +#define BYTE_POINTER unsigned char* +#define BUFFER_PARAM(name) unsigned char* name +#define DECLARE_READER(name, type) int name(type buffer) +#define READER_DECLARATIONS int written_by_definition(unsigned char* buffer); + +typedef unsigned char* byte_pointer; +#define BYTE_TYPEDEF byte_pointer + +// at line 23, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'buffer' in function 'object_macro' is never used to modify the pointed object +// ↳ current type: unsigned char *buffer +// ↳ suggested type: const unsigned char *buffer +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int object_macro(BYTE* buffer) +{ + return buffer[0]; +} + +// at line 33, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'buffer' in function 'pointer_macro' is never used to modify the pointed object +// ↳ current type: unsigned char *buffer +// ↳ suggested type: const unsigned char *buffer +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int pointer_macro(BYTE_POINTER buffer) +{ + return buffer[0]; +} + +// at line 43, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'buffer' in function 'declarator_macro' is never used to modify the pointed object +// ↳ current type: unsigned char *buffer +// ↳ suggested type: const unsigned char *buffer +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int declarator_macro(BUFFER_PARAM(buffer)) +{ + return buffer[0]; +} + +// at line 53, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'buffer' in function 'argument_macro' is never used to modify the pointed object +// ↳ current type: unsigned char *buffer +// ↳ suggested type: const unsigned char *buffer +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +DECLARE_READER(argument_macro, unsigned char*) +{ + return buffer[0]; +} + +// at line 63, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'buffer' in function 'typedef_macro' is never used to modify the pointed object +// ↳ current type: unsigned char *buffer +// ↳ suggested type: const unsigned char *buffer +// ↳ not checked with Clang: the parameter's typedef is written by a macro +int typedef_macro(BYTE_TYPEDEF buffer) +{ + return buffer[0]; +} + +READER_DECLARATIONS + +// at line 75, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'buffer' in function 'written_by_definition' is never used to modify the pointed object +// ↳ current type: unsigned char *buffer +// ↳ suggested type: const unsigned char *buffer +// ↳ not checked with Clang: the declaration is written by a macro definition +int written_by_definition(unsigned char* buffer) +{ + return buffer[0]; +} + +// A macro type, and a callback of the old type: the const version does not compile. +int object_macro_callback(BYTE* buffer) +{ + return buffer[0]; +} + +int apply(int (*reader)(BYTE*), BYTE* buffer); + +int run_callback(BYTE* buffer) +{ + return apply(object_macro_callback, buffer); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-must-compile.c b/test/pointer_reference-const_correctness/const-suggestion-must-compile.c new file mode 100644 index 0000000..cba23b5 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-must-compile.c @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a parameter is reported only if the analyzed files still compile with its suggested type +// and generate the same code. Here the const version does not compile, or selects other code. + +// strict-expectation-details: true +// not contains: parameter 'c' in function 'writer' +// not contains: parameter 'slot' in function 'read_slot' +// not contains: parameter 'p' in function 'address_to_int_pp' +// not contains: parameter 'p' in function 'generic_selection' +// not contains: ↳ ABI: + +struct ctx +{ + int* value; +}; + +void apply(void (*callback)(struct ctx*), struct ctx* c); + +// Passed where a callback with a non-const parameter is expected. +static void writer(struct ctx* c) +{ + (void)c->value; +} + +void run(struct ctx* c) +{ + apply(writer, c); +} + +// Its address initializes a pointer whose type has a non-const parameter. +static int read_slot(int* slot) +{ + return *slot; +} + +int (*const slot_reader)(int*) = read_slot; + +// &p is passed where int** is expected: const would differ in a nested qualifier. +static int read_pp(int** pp) +{ + return **pp; +} + +int address_to_int_pp(int* p) +{ + return read_pp(&p); +} + +// _Generic selects on the parameter's type: the const version compiles, but returns 0 instead +// of reading *p. +#define READ_ONLY(x) _Generic((x), const int*: 1, default: 0) + +int generic_selection(int* p) +{ + if (READ_ONLY(p)) + return 0; + return *p; +} + +// Control: nothing depends on the parameter's type. +// at line 66, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_value' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +int read_value(int* p) +{ + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-must-compile.cpp b/test/pointer_reference-const_correctness/const-suggestion-must-compile.cpp new file mode 100644 index 0000000..7797bda --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-must-compile.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a parameter is reported only if the analyzed files still compile with its suggested type. + +// strict-expectation-details: true +// not contains: parameter 'p' in function 'address_to_int_pp(int*)' +// not contains: parameter 'value' in function 'unused_lambda(int*)' +// not contains: parameter 'r' in function 'keep(int&)' + +// &p is passed where int** is expected: no function matches the const version. +static int read_pp(int** pp) +{ + return **pp; +} + +int address_to_int_pp(int* p) +{ + return read_pp(&p); +} + +// A writing lambda that is never called: its body is not in the generated code. +int unused_lambda(int* value) +{ + auto bump = [&]() { return ++*value; }; + (void)bump; + return 0; +} + +// A reference bound to an int& member of an aggregate. +struct Root +{ + int& ref; +}; + +void keep(int& r) +{ + Root root{r}; + (void)root; +} + +// Control: nothing depends on the parameter's type. +// at line 47, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_value(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_Z10read_valuePi -> _Z10read_valuePKi): code compiled separately must use the updated declaration and be recompiled +int read_value(int* p) +{ + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-override-elsewhere.cpp b/test/pointer_reference-const_correctness/const-suggestion-override-elsewhere.cpp new file mode 100644 index 0000000..63613d5 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-override-elsewhere.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: with const, Derived::read no longer overrides Base::read: it becomes another virtual +// function. The using-declaration keeps -Woverloaded-virtual silent, and the key functions are +// defined in another file, so this one emits no vtable and no call: its code changes only the +// method's symbol. The methods it overrides change. + +// not contains: parameter 'p' in function 'Derived::read(int*)' +struct Base +{ + virtual ~Base(); + virtual int read(int* p); +}; + +struct Derived : Base +{ + ~Derived() override; + using Base::read; + virtual int read(int* p); +}; + +int Derived::read(int* p) +{ + return *p; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-pointer-const-only.c b/test/pointer_reference-const_correctness/const-suggestion-pointer-const-only.c new file mode 100644 index 0000000..c7a0cbb --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-pointer-const-only.c @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217 (D13): a pointer that is const itself and whose pointed object is only read. The rule +// suggests const for the pointed object; the check tries the plan's candidate, which keeps the +// pointer's own const, and the suggestion displays that candidate. + +// strict-expectation-details: true + +// at line 18, column 0 +// [ !Info! ] ConstParameterNotModified.PointerConstOnly: parameter 'p' in function 'read_only' is declared 'int * const p' but the pointed object is never modified +// ↳ consider 'const int *const p' for API const-correctness +// ↳ current type: int * const p +// ↳ suggested type: const int *const p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// analyzer-arg: --experimental-const-suggestion-check + +int read_only(int* const p) +{ + return *p; +} diff --git a/test/pointer_reference-const_correctness/const-suggestion-preamble.c b/test/pointer_reference-const_correctness/const-suggestion-preamble.c new file mode 100644 index 0000000..1a2aee2 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-preamble.c @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: rewriting the declarations of first_value and second_value rewrites the header below, +// which holds a checked warning; local_value is declared in this file, and its rewrite leaves the +// header untouched. The header must be named the same way before and after a rewrite: with a +// relative input path, it was named relatively before and absolutely once the rewritten file was +// remapped, and its warning counted as new. The file name recalls the precompiled preamble, since +// removed, that this fixture also tested: the header sits in the file's preamble, its leading +// #include lines. + +// strict-expectation-details: true + +// at line 33, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'first_value' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// at line 38, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'q' in function 'second_value' is never used to modify the pointed object +// ↳ current type: int *q +// ↳ suggested type: const int *q +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// at line 43, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'r' in function 'local_value' is never used to modify the pointed object +// ↳ current type: int *r +// ↳ suggested type: const int *r +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +#include "const-suggestion-preamble.h" + +int local_value(int* r); + +int first_value(int* p) +{ + return *p; +} + +int second_value(int* q) +{ + return *q; +} + +int local_value(int* r) +{ + return *r; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-preamble.h b/test/pointer_reference-const_correctness/const-suggestion-preamble.h new file mode 100644 index 0000000..78fcd90 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-preamble.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef CONST_SUGGESTION_PREAMBLE_H +#define CONST_SUGGESTION_PREAMBLE_H + +// A checked warning, reported by every compilation of the file, before and after the rewrite: it +// must not count as new. +static inline int* without_const(const int* p) +{ + return p; +} + +int first_value(int* p); +int second_value(int* q); + +#endif diff --git a/test/pointer_reference-const_correctness/const-suggestion-printed-signature.c b/test/pointer_reference-const_correctness/const-suggestion-printed-signature.c new file mode 100644 index 0000000..f037fc6 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-printed-signature.c @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: __PRETTY_FUNCTION__ prints the function's signature, which the suggested type changes. The +// suggestion is kept, and the message says what the program prints differently. Any other use of +// that text, such as its size, changes the generated code in a way that no reference, override or +// constant condition explains: that suggestion is kept, not checked. + +// strict-expectation-details: true +// not contains: ↳ ABI: +#include + +// at line 17, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'print_and_read' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside, except for the differences below +// ↳ difference: the signature printed by __PRETTY_FUNCTION__ changes, from "int print_and_read(int *)" to "int print_and_read(const int *)" +int print_and_read(int* p) +{ + puts(__PRETTY_FUNCTION__); + return *p; +} + +// at line 28, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'signature_size' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ not checked with Clang: the generated code changes, and no reference, override or constant condition explains it: code changed in signature_size: +int signature_size(int* p) +{ + return *p ? (int)sizeof(__PRETTY_FUNCTION__) : 0; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-repeated-reference.cpp b/test/pointer_reference-const_correctness/const-suggestion-repeated-reference.cpp new file mode 100644 index 0000000..327a049 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-repeated-reference.cpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a macro that expands its argument twice writes two references at one site, both where +// Clang expands them and where they are written. Each one is compared with its counterpart: +// sizeof names counted(bool) before the rewrite and counted(const int*) after it, twice, and the +// size, so the generated code, stays the same. The suggestion is refuted. + +// not contains: parameter 'p' in function 'counted(int*)' + +int counted(int* p) +{ + return *p; +} + +int counted(bool flag); + +#define BOTH(e) (sizeof(e) + sizeof(e)) + +int size_of_both(const int* q) +{ + return static_cast(BOTH(counted(q))); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-same-code.cpp b/test/pointer_reference-const_correctness/const-suggestion-same-code.cpp new file mode 100644 index 0000000..158e54e --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-same-code.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: the const version compiles, but the generated code changes: a call, an override or a +// constant now resolves elsewhere. The parameter is not reported. + +// strict-expectation-details: true +// not contains: parameter 'p' in function 'overload_target(int*)' +// not contains: parameter 'p' in function 'Shape::area(int*)' +// not contains: parameter 'p' in function 'ReadDerived::read(int*)' +// not contains: parameter 'p' in function 'ReadBase::read(int*)' +// not contains: parameter 'p' in function 'sized_target(int*)' +// not contains: parameter 'p' in function 'branch_target(int*)' +// not contains: parameter 'p' in function 'self_typed(int*)' +#include + +// A caller with a pointer to const now calls this overload instead of the bool one. +int overload_target(int* p) +{ + return *p; +} + +int overload_target(bool flag) +{ + return flag ? 1 : 0; +} + +int call_overload(const int* q) +{ + return overload_target(q); +} + +// With const, ReadDerived::read no longer overrides ReadBase::read. The using-declaration keeps +// -Woverloaded-virtual silent: nothing warns. +struct ReadBase +{ + virtual ~ReadBase() = default; + virtual int read(int* p) + { + return *p; + } +}; + +struct ReadDerived : ReadBase +{ + using ReadBase::read; + int read(int* p) + { + return *p; + } +}; + +int call_read(ReadBase& object, int* p) +{ + return object.read(p); +} + +ReadBase base_reader; +ReadDerived derived_reader; + +int make_read(int* p) +{ + return call_read(derived_reader, p); +} + +// A virtual call now selects another overload: only its vtable slot and argument type change. +// Shape::area(int*) is defined out of line: it is the key function, the vtable is emitted here. +struct Shape +{ + virtual ~Shape() = default; + virtual int area(bool flag) + { + return flag ? 1 : 0; + } + virtual int area(int* p); +}; + +int Shape::area(int* p) +{ + return *p; +} + +int call_area(Shape& shape, const int* q) +{ + return shape.area(q); +} + +// sizeof selects an overload without calling it: the returned constant changes. +int sized_target(int* p) +{ + return *p; +} + +char sized_target(bool flag); + +int call_sized(const int* q) +{ + return static_cast(sizeof(sized_target(q))); +} + +// if constexpr chooses its branch from an unevaluated call. +int branch_target(int* p) +{ + return *p; +} + +char branch_target(bool flag); + +template int pick_branch(T q) +{ + if constexpr (sizeof(branch_target(q)) == 1) + return 10; + else + return 20; +} + +int call_branch(const int* q) +{ + return pick_branch(q); +} + +// The function inspects its own parameter's type. +int self_typed(int* p) +{ + if constexpr (std::is_const_v>) + return 1; + else + return *p; +} + +// Controls: the code stays the same. +// at line 136, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'called_directly(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_Z15called_directlyPi -> _Z15called_directlyPKi): code compiled separately must use the updated declaration and be recompiled +int called_directly(int* p) +{ + return *p; +} + +int call_directly(int* p) +{ + return called_directly(p); +} + +// at line 152, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'twice_target(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_Z12twice_targetPi -> _Z12twice_targetPKi): code compiled separately must use the updated declaration and be recompiled +int twice_target(int* p) +{ + return *p; +} + +int twice_target(bool flag) +{ + return flag ? 2 : 3; +} + +#define TWICE(x) (twice_target(x) == twice_target(true)) + +int call_twice(int* p) +{ + return TWICE(p); +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-typedef-qualifier.c b/test/pointer_reference-const_correctness/const-suggestion-typedef-qualifier.c new file mode 100644 index 0000000..abb554f --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-typedef-qualifier.c @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217 (D10): a pointer typedef with a qualifier written apart. The rewrite writes the candidate +// in place of the whole written type, so that the pointer keeps its own const: const IntPtr p +// becomes const int *const p. A qualifier after the name is already kept. A declaration and the +// definition that differ in the qualifier each keep their own. A qualifier written by a macro +// cannot be folded into the candidate: the suggestion is kept, not checked. + +// strict-expectation-details: true + +// at line 43, column 0 +// [ !Info! ] ConstParameterNotModified.PointerConstOnly: parameter 'p' in function 'before' is declared 'int * const p' but the pointed object is never modified +// ↳ consider 'const int *const p' for API const-correctness +// ↳ current type: int * const p +// ↳ suggested type: const int *const p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// at line 47, column 0 +// [ !Info! ] ConstParameterNotModified.PointerConstOnly: parameter 'p' in function 'after' is declared 'int * const p' but the pointed object is never modified +// ↳ consider 'const int *const p' for API const-correctness +// ↳ current type: int * const p +// ↳ suggested type: const int *const p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// at line 52, column 0 +// [ !Info! ] ConstParameterNotModified.PointerConstOnly: parameter 'p' in function 'declared' is declared 'int * const p' but the pointed object is never modified +// ↳ consider 'const int *const p' for API const-correctness +// ↳ current type: int * const p +// ↳ suggested type: const int *const p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// at line 56, column 0 +// [ !Info! ] ConstParameterNotModified.PointerConstOnly: parameter 'p' in function 'by_macro' is declared 'int * const p' but the pointed object is never modified +// ↳ consider 'const int *const p' for API const-correctness +// ↳ current type: int * const p +// ↳ suggested type: const int *const p +// ↳ not checked with Clang: the qualifier of the parameter's typedef is written by a macro + +// analyzer-arg: --experimental-const-suggestion-check + +typedef int* IntPtr; +#define CONST const + +int before(const IntPtr p) +{ + return *p; +} +int after(IntPtr const p) +{ + return *p; +} +int declared(IntPtr p); +int declared(const IntPtr p) +{ + return *p; +} +int by_macro(CONST IntPtr p) +{ + return *p; +} diff --git a/test/pointer_reference-const_correctness/const-suggestion-typedef-qualifier.cpp b/test/pointer_reference-const_correctness/const-suggestion-typedef-qualifier.cpp new file mode 100644 index 0000000..d0adf35 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-typedef-qualifier.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217 (D10): in C++ too, const IntPtr p becomes const int *const p, the pointer keeping its own +// const; a qualifier after the name is already kept. + +// strict-expectation-details: true + +// at line 27, column 0 +// [ !Info! ] ConstParameterNotModified.PointerConstOnly: parameter 'p' in function 'before(int*)' is declared 'int * const p' but the pointed object is never modified +// ↳ consider 'const int *const p' for API const-correctness +// ↳ current type: int * const p +// ↳ suggested type: const int *const p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_Z6beforePi -> _Z6beforePKi): code compiled separately must use the updated declaration and be recompiled + +// at line 31, column 0 +// [ !Info! ] ConstParameterNotModified.PointerConstOnly: parameter 'p' in function 'after(int*)' is declared 'int * const p' but the pointed object is never modified +// ↳ consider 'const int *const p' for API const-correctness +// ↳ current type: int * const p +// ↳ suggested type: const int *const p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_Z5afterPi -> _Z5afterPKi): code compiled separately must use the updated declaration and be recompiled + +// analyzer-arg: --experimental-const-suggestion-check + +typedef int* IntPtr; + +int before(const IntPtr p) +{ + return *p; +} +int after(IntPtr const p) +{ + return *p; +} diff --git a/test/pointer_reference-const_correctness/const-suggestion-unpaired-fact.cpp b/test/pointer_reference-const_correctness/const-suggestion-unpaired-fact.cpp new file mode 100644 index 0000000..40ec5b1 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-unpaired-fact.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: a fact that the rewrite leaves without a counterpart cannot be compared. Here p + Offset{} +// calls operator+(int*, Offset) before the rewrite; once p points to const, that operator no +// longer applies, and the built-in addition takes Offset through its conversion to int. The +// reference to operator+ has no counterpart after the rewrite, while the size, so the generated +// code, stays the same: the comparison is incomplete, and the suggestion is kept, not checked. +// The expectation stops before the file and the line. + +// strict-expectation-details: true + +struct Offset +{ + operator int() const + { + return 0; + } +}; + +int* operator+(int*, Offset); + +// at line 26, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read(int*)' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ not checked with Clang: the facts at +int read(int* p) +{ + return *p ? static_cast(sizeof(p + Offset{})) : 0; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-virtual-base.cpp b/test/pointer_reference-const_correctness/const-suggestion-virtual-base.cpp new file mode 100644 index 0000000..f3a336c --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-virtual-base.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: the base-object constructor (C2) of a class with a virtual base takes a hidden `vtt` after +// `this`. The check rewrites the parameter the suggestion names, found by its index among the +// parameters the source declares: `x`, in both variants. `y`, which the constructor writes +// through, is not rewritten. + +// strict-expectation-details: true + +struct V +{ + int v = 0; +}; + +struct D : virtual V +{ + int a, b; + D(int* x, int* y); +}; + +// at line 33, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'x' in function 'D::D(int*, int*)' is never used to modify the pointed object +// ↳ current type: int *x +// ↳ suggested type: const int *x +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_ZN1DC2EPiS0_ -> _ZN1DC2EPKiPi): code compiled separately must use the updated declaration and be recompiled + +// at line 33, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'x' in function 'D::D(int*, int*)' is never used to modify the pointed object +// ↳ current type: int *x +// ↳ suggested type: const int *x +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside +// ↳ ABI: the suggested type changes the symbol (_ZN1DC1EPiS0_ -> _ZN1DC1EPKiPi): code compiled separately must use the updated declaration and be recompiled +D::D(int* x, int* y) : a(*x), b(0) +{ + *y = 1; +} + +int make() +{ + int p = 1, q = 2; + D d(&p, &q); + return d.a + q; +} + +// The Clang check of const suggestions is experimental: it runs only with this option. +// analyzer-arg: --experimental-const-suggestion-check diff --git a/test/pointer_reference-const_correctness/const-suggestion-void-cast.c b/test/pointer_reference-const_correctness/const-suggestion-void-cast.c new file mode 100644 index 0000000..64560c7 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-void-cast.c @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217: `(void)read_value` converts the function to no other function type: the suggestion is +// checked and kept. An explicit cast to another function type keeps it, not checked +// (cross-tu-const-suggestion-cast.c). + +// strict-expectation-details: true + +// at line 16, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_value' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// analyzer-arg: --experimental-const-suggestion-check + +static int read_value(int* p) +{ + (void)read_value; + return *p; +} + +int use_value(void) +{ + int value = 1; + return read_value(&value); +} diff --git a/test/pointer_reference-const_correctness/const-suggestion-werror.c b/test/pointer_reference-const_correctness/const-suggestion-werror.c new file mode 100644 index 0000000..873b775 --- /dev/null +++ b/test/pointer_reference-const_correctness/const-suggestion-werror.c @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// #217 (D6): the project turns its warnings into errors (-Werror) and disables a warning group +// that the check tracks, which this file triggers: strip returns a pointer to const as a plain +// pointer. The check keeps each tracked group a warning in both of its compilations: the file +// compiles as it is, and the suggestion for read_value is checked. + +// strict-expectation-details: true + +// at line 20, column 0 +// [ !Info! ] ConstParameterNotModified.Pointer: parameter 'p' in function 'read_value' is never used to modify the pointed object +// ↳ current type: int *p +// ↳ suggested type: const int *p +// ↳ checked in the analyzed files only, with their compile commands: they compile with the suggested type, and their generated code matches, metadata included, once debug information, diagnostic metadata, descriptive names and renamed symbols are set aside + +// The project's compile command, which both the analysis and the check use. +// analyzer-arg: --compile-arg=-Werror +// analyzer-arg: --compile-arg=-Wno-incompatible-pointer-types-discards-qualifiers +// analyzer-arg: --experimental-const-suggestion-check + +int read_value(int* p) +{ + return *p; +} + +char* strip(const char* s) +{ + return s; +} diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-anonymous-read.cpp b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-anonymous-read.cpp new file mode 100644 index 0000000..074d9de --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-anonymous-read.cpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// A function in an anonymous namespace: no other file can name it, although its homonym in another +// file has the same mangled name. That homonym must not refute this one's suggestion. +namespace +{ + int helper(int* p) + { + return *p; + } +} // namespace + +int read_through_helper() +{ + int value = 1; + return helper(&value); +} diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-anonymous-write.cpp b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-anonymous-write.cpp new file mode 100644 index 0000000..bd3aaf4 --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-anonymous-write.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// A homonym of cross-tu-const-suggestion-anonymous-read.cpp's helper, in this file's anonymous +// namespace, which writes through its parameter. +namespace +{ + int helper(int* p) + { + *p = 2; + return 0; + } +} // namespace + +int write_through_helper() +{ + int value = 1; + return helper(&value); +} diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-api.h b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-api.h new file mode 100644 index 0000000..666b4f5 --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-api.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef CROSS_TU_CONST_SUGGESTION_API_H +#define CROSS_TU_CONST_SUGGESTION_API_H + +int read_value(int* p); + +#endif diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-callback.c b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-callback.c new file mode 100644 index 0000000..0eafe43 --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-callback.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Another file stores the function where a non-const parameter is expected: with the suggested +// type, this file no longer compiles. +#include "cross-tu-const-suggestion-api.h" + +int (*const value_reader)(int*) = read_value; diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-caller.c b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-caller.c new file mode 100644 index 0000000..37d63bb --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-caller.c @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Another file calls the function through the header: it compiles with the suggested type. +#include "cross-tu-const-suggestion-api.h" + +int call_value(void) +{ + int value = 1; + return read_value(&value); +} diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-cast.c b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-cast.c new file mode 100644 index 0000000..0a2e084 --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-cast.c @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Another file converts the function's address with an explicit cast: the cast would accept the +// new type silently. +#include "cross-tu-const-suggestion-api.h" + +typedef int (*erased_reader)(void*); + +erased_reader erase_reader(void) +{ + return (erased_reader)read_value; +} diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-def.c b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-def.c new file mode 100644 index 0000000..88848d2 --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-def.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "cross-tu-const-suggestion-api.h" + +int read_value(int* p) +{ + return *p; +} diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-ir-caller.ll b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-ir-caller.ll new file mode 100644 index 0000000..cf0bb95 --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-ir-caller.ll @@ -0,0 +1,43 @@ +; ModuleID = 'cross-tu-const-suggestion-ir-caller.c' +source_filename = "cross-tu-const-suggestion-ir-caller.c" +target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "arm64-apple-macosx15.0.0" + +; Function Attrs: noinline nounwind optnone ssp uwtable(sync) +define i32 @call_value_from_ir() #0 !dbg !10 { +entry: + %value = alloca i32, align 4 + #dbg_declare(ptr %value, !15, !DIExpression(), !16) + store i32 1, ptr %value, align 4, !dbg !16 + %call = call i32 @read_value(ptr noundef %value), !dbg !17 + ret i32 %call, !dbg !18 +} + +declare i32 @read_value(ptr noundef) #1 + +attributes #0 = { noinline nounwind optnone ssp uwtable(sync) "frame-pointer"="non-leaf" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="apple-m1" "target-features"="+aes,+altnzcv,+ccdp,+ccidx,+ccpp,+complxnum,+crc,+dit,+dotprod,+flagm,+fp-armv8,+fp16fml,+fptoint,+fullfp16,+jsconv,+lse,+neon,+pauth,+perfmon,+predres,+ras,+rcpc,+rdm,+sb,+sha2,+sha3,+specrestrict,+ssbs,+v8.1a,+v8.2a,+v8.3a,+v8.4a,+v8a,+zcm,+zcz" } +attributes #1 = { "frame-pointer"="non-leaf" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="apple-m1" "target-features"="+aes,+altnzcv,+ccdp,+ccidx,+ccpp,+complxnum,+crc,+dit,+dotprod,+flagm,+fp-armv8,+fp16fml,+fptoint,+fullfp16,+jsconv,+lse,+neon,+pauth,+perfmon,+predres,+ras,+rcpc,+rdm,+sb,+sha2,+sha3,+specrestrict,+ssbs,+v8.1a,+v8.2a,+v8.3a,+v8.4a,+v8a,+zcm,+zcz" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6} +!llvm.dbg.cu = !{!7} +!llvm.ident = !{!9} + +!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 15, i32 5]} +!1 = !{i32 7, !"Dwarf Version", i32 5} +!2 = !{i32 2, !"Debug Info Version", i32 3} +!3 = !{i32 1, !"wchar_size", i32 4} +!4 = !{i32 8, !"PIC Level", i32 2} +!5 = !{i32 7, !"uwtable", i32 1} +!6 = !{i32 7, !"frame-pointer", i32 1} +!7 = distinct !DICompileUnit(language: DW_LANG_C11, file: !8, producer: "Homebrew clang version 20.1.2", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: Apple, sysroot: "/Library/Developer/CommandLineTools/SDKs/MacOSX15.sdk", sdk: "MacOSX15.sdk") +!8 = !DIFile(filename: "cross-tu-const-suggestion-ir-caller.c", directory: ".", checksumkind: CSK_MD5, checksum: "c0dbcf087f44a6adb4189892f747b393") +!9 = !{!"Homebrew clang version 20.1.2"} +!10 = distinct !DISubprogram(name: "call_value_from_ir", scope: !8, file: !8, line: 3, type: !11, scopeLine: 4, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !7, retainedNodes: !14) +!11 = !DISubroutineType(types: !12) +!12 = !{!13} +!13 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed) +!14 = !{} +!15 = !DILocalVariable(name: "value", scope: !10, file: !8, line: 5, type: !13) +!16 = !DILocation(line: 5, column: 9, scope: !10) +!17 = !DILocation(line: 6, column: 12, scope: !10) +!18 = !DILocation(line: 6, column: 5, scope: !10) diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-redecl.c b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-redecl.c new file mode 100644 index 0000000..d123905 --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-redecl.c @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Another file declares the function itself, without the header: rewriting the header would +// leave this declaration with the old type. +int read_value(int* p); + +int read_through_own_declaration(int* q) +{ + return read_value(q); +} diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-static-read.c b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-static-read.c new file mode 100644 index 0000000..51aa21c --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-static-read.c @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// A static function: no other file can name it. A static homonym in another file is another +// function, and must not refute this one's suggestion. +static int helper(int* p) +{ + return *p; +} + +int read_through_helper(void) +{ + int value = 1; + return helper(&value); +} diff --git a/test/pointer_reference-const_correctness/cross-tu-const-suggestion-static-write.c b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-static-write.c new file mode 100644 index 0000000..ddee093 --- /dev/null +++ b/test/pointer_reference-const_correctness/cross-tu-const-suggestion-static-write.c @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// A static homonym of cross-tu-const-suggestion-static-read.c's helper, which writes through its +// parameter. +static int helper(int* p) +{ + *p = 2; + return 0; +} + +int write_through_helper(void) +{ + int value = 1; + return helper(&value); +} diff --git a/test/unit/analyzer_module_unit_tests.cpp b/test/unit/analyzer_module_unit_tests.cpp index 698b961..87de16c 100644 --- a/test/unit/analyzer_module_unit_tests.cpp +++ b/test/unit/analyzer_module_unit_tests.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #include "StackUsageAnalyzer.hpp" #include "analysis/AnalyzerUtils.hpp" +#include "analysis/ConstSuggestionCheck.hpp" +#include "analysis/ConstSuggestionInternal.hpp" #include "app/CrossTUSummaryDriver.hpp" #include "app/AnalyzerApp.hpp" #include "cli/ArgParser.hpp" @@ -21,12 +23,15 @@ #include "analyzer/DiagnosticEmitter.hpp" #include "analyzer/LocationResolver.hpp" #include "analyzer/ModulePreparationService.hpp" +#include "mangle.hpp" #include #include #include #include #include +#include +#include #include #include #include @@ -749,6 +754,793 @@ namespace return report.failures == 0; } + /// #217: the code generated before and after a const rewrite is compared once the symbols + /// the rewrite renames are paired, by the pairs its correspondences established and by them + /// only: a symbol renamed without a pair, even one whose name differs only by const, is a + /// difference. + bool testConstSuggestionCodeComparison(TestReport& report) + { + using ctrace::stack::analysis::rewrittenCodeDifference; + using ctrace::stack::analysis::SymbolPair; + llvm::LLVMContext context; + llvm::SMDiagnostic error; + const auto parse = + [&](std::string text, const std::string& function, const std::string& typeInfo) + { + for (const auto& [from, to] : {std::pair{std::string("@F"), "@" + function}, + std::pair{std::string("@TI"), "@" + typeInfo}}) + { + for (std::size_t at = text.find(from); at != std::string::npos; + at = text.find(from, at + to.size())) + { + text.replace(at, from.size(), to); + } + } + return llvm::parseAssemblyString(text, error, context); + }; + // The rewritten function and its caller. + const std::string called = "define i32 @F(ptr %p) {\n" + " %v = load i32, ptr %p\n" + " ret i32 %v\n" + "}\n" + "define i32 @_Z3usePi(ptr %p) {\n" + " %r = call i32 @F(ptr %p)\n" + " ret i32 %r\n" + "}\n"; + // The rewritten function throws its parameter: the thrown type changes with it, and a + // handler `catch (int*)` elsewhere no longer catches it. + const std::string thrown = "@TI = external constant ptr\n" + "declare ptr @__cxa_allocate_exception(i64)\n" + "declare void @__cxa_throw(ptr, ptr, ptr)\n" + "define void @F(ptr %p) {\n" + " %e = call ptr @__cxa_allocate_exception(i64 8)\n" + " store ptr %p, ptr %e\n" + " call void @__cxa_throw(ptr %e, ptr @TI, ptr null)\n" + " unreachable\n" + "}\n"; + const std::unique_ptr calledBefore = parse(called, "_Z1fPi", ""); + const std::unique_ptr calledAfter = parse(called, "_Z1fPKi", ""); + const std::unique_ptr thrownBefore = parse(thrown, "_Z1fPi", "_ZTIPi"); + const std::unique_ptr thrownAfter = parse(thrown, "_Z1fPKi", "_ZTIPKi"); + if (!calledBefore || !calledAfter || !thrownBefore || !thrownAfter) + { + report.expect(false, "ConstSuggestionCode setup: the IR does not parse"); + return false; + } + const std::vector function = {{"_Z1fPi", "_Z1fPKi"}}; + report.expect(!rewrittenCodeDifference(*calledBefore, *calledAfter, function), + "ConstSuggestionCode: the rewritten function and its caller are renamed " + "together"); + report.expect(rewrittenCodeDifference(*calledBefore, *calledAfter, {}).has_value(), + "ConstSuggestionCode: a function renamed without an established pair is a " + "difference"); + report.expect(rewrittenCodeDifference(*thrownBefore, *thrownAfter, function).has_value(), + "ConstSuggestionCode: typeinfo for int* and for int const*, without a pair, " + "are not paired"); + return report.failures == 0; + } + + /// #217: the code comparison sets aside what carries no meaning for an execution: debug + /// information, diagnostic metadata, the descriptive names of alias scopes, the numbers of + /// metadata and the comments of the printed IR. Every other metadata is compared by contents. + bool testConstSuggestionNormalizations(TestReport& report) + { + using ctrace::stack::analysis::rewrittenCodeDifference; + llvm::LLVMContext context; + llvm::SMDiagnostic error; + const auto differ = [&](const std::string& before, const std::string& after) + { + const std::unique_ptr was = + llvm::parseAssemblyString(before, error, context); + const std::unique_ptr now = + llvm::parseAssemblyString(after, error, context); + if (!was || !now) + return std::optional(); + return std::optional(rewrittenCodeDifference(*was, *now, {}).has_value()); + }; + const auto replaced = [](std::string text, const std::string& from, const std::string& to) + { + text.replace(text.find(from), from.size(), to); + return text; + }; + const std::string range = "define i32 @f(ptr %p) {\n" + " %v = load i32, ptr %p, !range !0\n" + " ret i32 %v\n" + "}\n" + "!0 = !{i32 0, i32 10}\n"; + const std::string debug = + "define i32 @f(ptr %p) !dbg !3 {\n" + " %v = load i32, ptr %p, !dbg !6\n" + " ret i32 %v, !dbg !6\n" + "}\n" + "!llvm.dbg.cu = !{!0}\n" + "!llvm.module.flags = !{!2}\n" + "!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, emissionKind: " + "FullDebug)\n" + "!1 = !DIFile(filename: \"a.c\", directory: \"/\")\n" + "!2 = !{i32 2, !\"Debug Info Version\", i32 3}\n" + "!3 = distinct !DISubprogram(name: \"f\", scope: !1, file: !1, line: 1, type: !4, " + "spFlags: DISPFlagDefinition, unit: !0)\n" + "!4 = !DISubroutineType(types: !5)\n" + "!5 = !{}\n" + "!6 = !DILocation(line: 2, column: 3, scope: !3)\n"; + const std::string srcloc = "define void @f() {\n" + " call void asm sideeffect \"nop\", \"\"(), !srcloc !0\n" + " ret void\n" + "}\n" + "!0 = !{i64 100}\n"; + const std::string annotation = "define void @f() {\n" + " %a = alloca i32, align 4, !annotation !0\n" + " ret void\n" + "}\n" + "!0 = !{!\"auto-init\"}\n"; + const std::string scopes = + "declare void @llvm.experimental.noalias.scope.decl(metadata)\n" + "define void @f(ptr %p, ptr %q) {\n" + " call void @llvm.experimental.noalias.scope.decl(metadata !2)\n" + " %v = load i32, ptr %p, align 4, !alias.scope !2, !noalias !3\n" + " store i32 %v, ptr %q, align 4, !alias.scope !3, !noalias !2\n" + " ret void\n" + "}\n" + "!0 = distinct !{!0, !\"domain of f(int*)\"}\n" + "!1 = distinct !{!1, !0, !\"f(int*): %p\"}\n" + "!4 = distinct !{!4, !0, !\"f(int*): %q\"}\n" + "!2 = !{!1}\n" + "!3 = !{!4}\n"; + const std::string flags = "define void @f() {\n" + " ret void\n" + "}\n" + "!llvm.module.flags = !{!0}\n" + "!0 = !{i32 1, !\"wchar_size\", i32 4}\n"; + // The same two functions, defined in the other order: the printer numbers their loop + // metadata in the other order. + const std::string loopF = "define void @f() {\n" + "entry:\n" + " br label %a\n" + "a:\n" + " br label %a, !llvm.loop !0\n" + "}\n"; + const std::string loopG = "define void @g() {\n" + "entry:\n" + " br label %b\n" + "b:\n" + " br label %b, !llvm.loop !1\n" + "}\n"; + const std::string loops = "!0 = distinct !{!0, !2}\n" + "!1 = distinct !{!1, !3}\n" + "!2 = !{!\"llvm.loop.mustprogress\"}\n" + "!3 = !{!\"llvm.loop.unroll.disable\"}\n"; + const std::optional orderChanged = + differ(loopF + loopG + loops, loopG + loopF + loops); + // The same code, its join block's predecessors listed in the other order: only the + // "; preds" comment of the printed IR changes. + const std::string join = "define i32 @f(i1 %c) {\n" + "entry:\n" + " br i1 %c, label %a, label %b\n" + "a:\n" + " br label %join\n" + "b:\n" + " br label %join\n" + "join:\n" + " %v = phi i32 [ 1, %a ], [ 2, %b ]\n" + " ret i32 %v\n" + "}\n"; + const std::optional predecessorsChanged = + differ(join, replaced(join, " ret i32 %v\n", + " ret i32 %v\n uselistorder label %join, { 1, 0 }\n")); + const std::optional rangeChanged = + differ(range, replaced(range, "i32 0, i32 10", "i32 0, i32 5")); + const std::optional debugChanged = + differ(debug, replaced(debug, "column: 3", "column: 9")); + const std::optional srclocChanged = + differ(srcloc, replaced(srcloc, "i64 100", "i64 200")); + const std::optional annotationChanged = + differ(annotation, replaced(annotation, "auto-init", "other")); + const std::optional namesChanged = differ( + scopes, + replaced(replaced(replaced(scopes, "domain of f(int*)", "domain of f(const int*)"), + "f(int*): %p", "f(const int*): %p"), + "f(int*): %q", "f(const int*): %q")); + const std::optional scopesChanged = + differ(scopes, replaced(scopes, "!alias.scope !3, !noalias !2", + "!alias.scope !2, !noalias !2")); + const std::optional flagsChanged = differ(flags, replaced(flags, "i32 4}", "i32 2}")); + if (!orderChanged || !predecessorsChanged || !rangeChanged || !debugChanged || + !srclocChanged || !annotationChanged || !namesChanged || !scopesChanged || + !flagsChanged) + { + report.expect(false, "ConstSuggestionMetadata setup: the IR does not parse: " + + error.getMessage().str()); + return false; + } + report.expect(!*orderChanged, + "ConstSuggestionMetadata: metadata numbered in another order are the same"); + report.expect(!*predecessorsChanged, + "ConstSuggestionCode: predecessors listed in another order are the same"); + report.expect(*rangeChanged, "ConstSuggestionMetadata: another !range is a difference"); + report.expect(!*debugChanged, "ConstSuggestionMetadata: debug information is not compared"); + report.expect(!*srclocChanged, + "ConstSuggestionMetadata: the source position of inline assembly is not " + "compared"); + report.expect(!*annotationChanged, "ConstSuggestionMetadata: annotations are not compared"); + report.expect(!*namesChanged, + "ConstSuggestionMetadata: the descriptive names of alias scopes are not " + "compared"); + report.expect(*scopesChanged, + "ConstSuggestionMetadata: another alias scope on an access is a difference"); + report.expect(*flagsChanged, + "ConstSuggestionMetadata: another module flag is a difference"); + return report.failures == 0; + } + + /// #217 (D10, D18): step 3 detects a rewrite that loses the parameter's own top-level const. + /// The plan of read_only(int *const p) is executed with an edit that writes `const int *` + /// instead of `const int *const`: the declaration does not get the candidate of its type, + /// whatever the planner's later edits are. + bool testConstSuggestionTopLevelQualifierLost(TestReport& report) + { + namespace cs = ctrace::stack::analysis::const_suggestion; + const std::filesystem::path dir = + std::filesystem::temp_directory_path() / "ct_const_suggestion_top_level_lost"; + std::filesystem::create_directories(dir); + const std::filesystem::path file = dir / "k.c"; + const std::string text = "int read_only(int *const p) { return *p; }\n"; + std::ofstream(file) << text; + const ctrace::stack::analysis::UnitCommand command{{"clang", "-c", file.string()}, + dir.string()}; + std::vector targets(1); + targets[0].symbol = "read_only"; + targets[0].sourceIndex = 0; + const cs::UnitFacts before = cs::inspect(command, nullptr, {}, targets, "test", false); + if (before.entities.size() != 1 || before.entities[0].plan.edits.size() != 1) + { + report.expect(false, "ConstSuggestionTopLevel setup: read_only is not planned"); + return false; + } + cs::SuggestionPlan lost = before.entities[0].plan; + const std::string written = "int *const"; + const std::size_t at = text.find(written); + lost.edits.begin()->second = {{at, written.size(), "const int *", 1}}; + std::string rewritten = text; + rewritten.replace(at, written.size(), "const int *"); + cs::Target target = targets[0]; + target.plan = &lost; + target.expected = before.entities[0].expected; + const cs::UnitFacts after = cs::inspect( + command, &lost, {{lost.edits.begin()->first, rewritten}}, {target}, "test", false); + report.expect(after.entities.size() == 1 && after.entities[0].withoutCandidate, + "ConstSuggestionTopLevel: a rewrite that loses the pointer's own const does " + "not get the candidate"); + return report.failures == 0; + } + + /// A compile command for a C++ source, as the Clang check builds one: compilerlib's driver + /// settings (Clang's resource directory, the SDK on macOS) before the arguments, so that the + /// standard library's headers are found on every platform. + ctrace::stack::analysis::UnitCommand cxxCommand(const std::filesystem::path& file) + { + const std::vector args{"-std=c++20", "-c", file.string()}; + ctrace::stack::analysis::UnitCommand out{{"clang++"}, file.parent_path().string()}; + compilerlib::DriverConfig driver; + std::string error; + if (compilerlib::resolveDriverConfig(args, driver, error)) + { + out.args = {driver.clang_path}; + if (driver.force_cxx_driver) + out.args.push_back("--driver-mode=g++"); + if (driver.add_resource_dir) + { + out.args.push_back("-resource-dir"); + out.args.push_back(driver.resource_dir); + } + if (driver.add_sysroot) + { + out.args.push_back("-isysroot"); + out.args.push_back(driver.sysroot); + } + } + out.args.insert(out.args.end(), args.begin(), args.end()); + return out; + } + + // run_test.py's P2_KEYS_SOURCE with a polymorphic local class, whose destructor has a deleting + // and a base variant. + constexpr const char* kNestedKeysSourceWithPoly = R"(#include + +template struct Box +{ + T* value; + friend bool operator==(const Box& a, const Box& b) { return a.value == b.value; } + struct Inner + { + int n = 0; + }; +}; + +template int probe(T) { return 1; } + +template int apply_both(L call) +{ + int (*pointer)(int) = call; + return pointer(1) + call(1); +} + +template int pass(int n, Each&& each) +{ + int total = 0; + each(n, [&](int i) { total += i; }); + return total; +} + +int drive(int n) +{ + return pass(n, [&](int count, auto&& body) { + struct State + { + std::vector v; + }; + State a{{count}}; + State b = a; + State c = static_cast(a); + b = c; + c = static_cast(b); + State d; + struct Poly + { + virtual ~Poly() {} + }; + delete new Poly; + Box x{nullptr}, y{nullptr}; + const int found = (x == y) + probe(typename Box::Inner{}) + + probe(typename Box::Inner{}) + + probe(typename Box::Inner{}) + + apply_both([](int v) { return 2 * v; }); + body(found + static_cast(b.v.size() + c.v.size() + d.v.size())); + }); +} +)"; + + /// #217 (§5.7, step 5): the correspondences of the types nested in established pairs and of the + /// specializations whose arguments hold them (§5.7): only the symbols that the rewrite renames, + /// one to one, every other part of a key kept. Synthetic facts: what Clang writes into a key is + /// pinned by testConstSuggestionNestedIdentities. + bool testConstSuggestionNestedCorrespondences(TestReport& report) + { + namespace cs = ctrace::stack::analysis::const_suggestion; + // The entity before and after the rewrite, paired by its variant (step 1). + const std::string was = "_Z1fIiEviOT_"; + const std::string now = "_Z1fIiEviRKT_"; + // A type nested in a function: its record's site and the symbol of the function around it. + const auto nested = [](const std::string& site, const std::string& around) + { return "\x01" + site + "\x02" + around + "\x03"; }; + // A specialization's key: variant, pattern site, its arguments, its function type. + const auto spec = + [&](const std::string& variant, const std::string& argument, const std::string& other) + { + return variant + "|use.cpp:3|(" + argument + ", " + other + ")[void (" + argument + + " &&)]"; + }; + const auto pairsOf = [&](std::vector before, + std::vector after, std::string& missing) + { + cs::UnitFacts wasUnit; + wasUnit.entities.resize(1); + wasUnit.entities[0].variants = {{was, "", ""}}; + wasUnit.identities = std::move(before); + cs::UnitFacts nowUnit; + nowUnit.entities.resize(1); + nowUnit.entities[0].variants = {{now, "", ""}}; + nowUnit.identities = std::move(after); + std::vector out; + for (const ctrace::stack::analysis::SymbolPair& pair : + cs::correspond(wasUnit, 0, nowUnit, missing)) + { + if (pair.before != was) + out.push_back(pair.before + "->" + pair.after); + } + return out; + }; + using Pairs = std::vector; + const std::string inWas = nested("use.cpp:40", was); + const std::string inNow = nested("use.cpp:40", now); + std::string missing; + + // A specialization whose argument is a type nested in the entity pairs through the + // entity's pair. + report.expect(pairsOf({{"_Z1aOld", "", "", spec("", inWas, "int")}}, + {{"_Z1aNew", "", "", spec("", inNow, "int")}}, + missing) == Pairs{"_Z1aOld->_Z1aNew"} && + missing.empty(), + "ConstSuggestionNested: a specialization pairs through its nested argument"); + + // Every other argument is kept: int against long, no pair (mutant: a key without the + // arguments that hold no nested type). + missing.clear(); + report.expect(pairsOf({{"_Z1aOld", "", "", spec("", inWas, "int")}}, + {{"_Z1aNew", "", "", spec("", inNow, "long")}}, missing) + .empty(), + "ConstSuggestionNested: another argument differs, no pair"); + + // Refinement 1, only renamed symbols: a symbol present on both sides at the same key + // stays out, and the renamed one pairs (without that filter: two candidates on each + // side, no pair, a reason). The nested type is written in a template, without an + // enclosing symbol, so its key does not change with the rewrite. + const std::string inTemplate = nested("use.cpp:50", ""); + missing.clear(); + report.expect(pairsOf({{"_Z1bOld", "", "", spec("", inTemplate, "int")}, + {"_Z1bSame", "", "", spec("", inTemplate, "int")}}, + {{"_Z1bNew", "", "", spec("", inTemplate, "int")}, + {"_Z1bSame", "", "", spec("", inTemplate, "int")}}, + missing) == Pairs{"_Z1bOld->_Z1bNew"} && + missing.empty(), + "ConstSuggestionNested: a symbol the rewrite does not rename takes no part"); + + // One to one: two before and two after at one key, no pair and a reason, not the + // first candidates paired. + missing.clear(); + report.expect(pairsOf({{"_Z1cOne", "", "", spec("", inWas, "int")}, + {"_Z1cTwo", "", "", spec("", inWas, "int")}}, + {{"_Z1cThree", "", "", spec("", inNow, "int")}, + {"_Z1cFour", "", "", spec("", inNow, "int")}}, + missing) + .empty() && + missing.find("has no one-to-one counterpart") != std::string::npos, + "ConstSuggestionNested: two candidates on each side, no pair, a reason"); + + // One before, two after: no pair, a reason. + missing.clear(); + report.expect(pairsOf({{"_Z1dOne", "", "", spec("", inWas, "int")}}, + {{"_Z1dTwo", "", "", spec("", inNow, "int")}, + {"_Z1dThree", "", "", spec("", inNow, "int")}}, + missing) + .empty() && + missing.find("has no one-to-one counterpart") != std::string::npos, + "ConstSuggestionNested: one candidate becomes two, no pair, a reason"); + + // The ABI variant is part of the key: C1 before, C2 after, no pair. + missing.clear(); + report.expect(pairsOf({{"_Z1eOld", "", "", spec("C1", inWas, "int")}}, + {{"_Z1eNew", "", "", spec("C2", inNow, "int")}}, missing) + .empty(), + "ConstSuggestionNested: two variants never pair"); + + // A member of a nested type pairs once the function around it is paired; around a + // function without a pair, it does not. + const std::string member = "lambda |use.cpp:40|use.cpp:40|operator() const|int (int)"; + missing.clear(); + report.expect(pairsOf({{"_ZOldL", member, was, ""}}, {{"_ZNewL", member, now, ""}}, + missing) == Pairs{"_ZOldL->_ZNewL"}, + "ConstSuggestionNested: a lambda's member pairs through its function"); + missing.clear(); + report.expect( + pairsOf({{"_ZOldL", member, "_Z1hv", ""}}, {{"_ZNewL", member, now, ""}}, missing) + .empty(), + "ConstSuggestionNested: around an unpaired function, no pair"); + return report.failures == 0; + } + + /// #217 (§5.7, step 5): what Clang writes into the keys of the nested entities, on one compilation of + /// real code. Every expected member and specialization has an identity, and two symbols + /// never share a key: a local class's implicit members, a lambda's call operator and static + /// invoker, a virtual destructor's variants, a class template's hidden friend, and member + /// classes of two specializations that differ only by a closure type. + bool testConstSuggestionNestedIdentities(TestReport& report) + { + namespace cs = ctrace::stack::analysis::const_suggestion; + const std::filesystem::path dir = + std::filesystem::temp_directory_path() / "ct_const_suggestion_nested_identities"; + std::filesystem::create_directories(dir); + const std::filesystem::path file = dir / "keys.cpp"; + // checks.py's P2_KEYS_SOURCE, with a polymorphic local class for the destructor's + // variants (its vtable is a global that P2 does not pair, so the end-to-end check + // leaves it out). + std::ofstream(file) << kNestedKeysSourceWithPoly; + const ctrace::stack::analysis::UnitCommand command = cxxCommand(file); + std::vector targets(1); + targets[0].symbol = "_Z4passIZ5driveiE3$_0EiiOT_"; + targets[0].sourceIndex = 0; + const cs::UnitFacts unit = cs::inspect(command, nullptr, {}, targets, "test", false); + // How many identities a mangled or demangled part selects, by kind of key. + const auto count = [&](const std::string& part, bool specialization) + { + return std::count_if(unit.identities.begin(), unit.identities.end(), + [&](const cs::UnitFacts::Identity& identity) + { + const std::string& key = + specialization ? identity.specialization : identity.nested; + return !key.empty() && + (identity.symbol.find(part) != std::string::npos || + llvm::demangle(identity.symbol).find(part) != + std::string::npos); + }); + }; + // Presence: each refinement's candidates are identified: the + // default, copy and move constructors (State d, b, c), the two assignments. + report.expect(count("::State::State(", false) >= 3 && + count("::State::operator=(", false) == 2, + "ConstSuggestionNestedKeys: the local class's implicit members are there"); + report.expect(count("__invoke", false) == 1, "ConstSuggestionNestedKeys: the static " + "invoker is there"); + report.expect(count("4PolyD0Ev", false) == 1 && count("4PolyD2Ev", false) == 1, + "ConstSuggestionNestedKeys: the destructor's deleting and base variants " + "are there"); + report.expect(count("operator==", true) == 1, + "ConstSuggestionNestedKeys: the hidden friend is identified (by its " + "function type)"); + report.expect(count("::Inner>(", true) == 3, + "ConstSuggestionNestedKeys: probe of the three member classes is identified " + "(through their specialization, which holds the nested type)"); + // Distinctness: no two symbols share a key (without the member's type, the implicit + // constructors and assignments merge; without the name, the static flag and the + // qualifiers together, which the test protects collectively, not each one, operator() and + // __invoke merge; without the variant, D0 and D2 merge; a member + // class written without its specialization's arguments: probe's two closures merge). + std::map owner; + bool apart = true; + for (const cs::UnitFacts::Identity& identity : unit.identities) + { + for (const std::string& key : + {identity.nested.empty() ? "" : "n" + identity.nested + '\0' + identity.enclosing, + identity.specialization.empty() ? "" : "s" + identity.specialization}) + { + if (key.empty()) + continue; + const auto [it, added] = owner.try_emplace(key, identity.symbol); + apart = apart && (added || it->second == identity.symbol); + } + } + report.expect(apart, "ConstSuggestionNestedKeys: two symbols never share a key"); + return report.failures == 0; + } + + // The implicit members of classes whose arguments hold a type nested in the generic lambda's + // call operator (P2, implicit-member extension): Box's six, Derived's virtual + // destructor (D0 and D2), Holder's and Holder's destructors; and a + // class whose arguments hold no nested type, Plain, whose members must not be identified. + constexpr const char* kImplicitMembersSource = R"(#include + +struct Base +{ + virtual ~Base() {} +}; + +template struct Box +{ + T value; +}; + +template struct Derived : Base +{ + T value; +}; + +template struct Holder +{ + T value; + U other; +}; + +template struct Plain +{ + std::vector items; +}; + +template int pass(int n, Each&& each) +{ + int total = 0; + each(n, [&](int i) { total += i; }); + return total; +} + +int drive(int n) +{ + return pass(n, [&](int count, auto&& body) { + struct State + { + std::vector v; + }; + Box a{{{count}}}; + Box b = a; + Box c = static_cast&&>(a); + b = c; + c = static_cast&&>(b); + Box d; + delete new Derived; + Holder h{}; + Holder k{}; + Plain plain; + body(static_cast(b.value.v.size() + c.value.v.size() + d.value.v.size() + + plain.items.size()) + + h.other + static_cast(k.other)); + }); +} +)"; + + /// #217 (§5.7, step 5): an implicit member of a class whose key holds a nested type is + /// identified by that class, its kind and name, its variant and its whole type. Every expected + /// one is there, a class without a nested type gets none, and no two symbols share a key: the + /// copy and move constructors (the type), D0 and D2 (the variant), Holder and + /// Holder (the class's other arguments). Limit: the kind and the name are not + /// shown to be needed. On every real case found, the variant (C against D) or the type already + /// separates two implicit members, so a key without them passes these tests too; they are kept, + /// and protected only together with the variant and the type. + bool testConstSuggestionImplicitMemberIdentities(TestReport& report) + { + namespace cs = ctrace::stack::analysis::const_suggestion; + const std::filesystem::path dir = + std::filesystem::temp_directory_path() / "ct_const_suggestion_implicit_members"; + std::filesystem::create_directories(dir); + const std::filesystem::path file = dir / "implicit.cpp"; + std::ofstream(file) << kImplicitMembersSource; + const ctrace::stack::analysis::UnitCommand command = cxxCommand(file); + std::vector targets(1); + targets[0].symbol = "_Z4passIZ5driveiE3$_0EiiOT_"; + targets[0].sourceIndex = 0; + const cs::UnitFacts unit = cs::inspect(command, nullptr, {}, targets, "test", false); + // The implicit-member keys whose symbol holds a mangled or demangled part. + const auto count = [&](const std::string& part) + { + return std::count_if(unit.identities.begin(), unit.identities.end(), + [&](const cs::UnitFacts::Identity& identity) + { + return identity.specialization.rfind("implicit ", 0) == 0 && + (identity.symbol.find(part) != std::string::npos || + llvm::demangle(identity.symbol).find(part) != + std::string::npos); + }); + }; + report.expect(count("Box<") >= 6, + "ConstSuggestionImplicitMembers: Box's default, copy and move " + "constructors, assignments and destructor are identified"); + report.expect(count("DerivedI") >= 2 && count("D0Ev") >= 1 && count("D2Ev") >= 1, + "ConstSuggestionImplicitMembers: Derived's deleting and base " + "destructors are identified"); + // Each Holder's destructor, by its mangled class (State then int, State then long): one variant + // where the base and complete destructors are aliases (ELF), both where they are not (Darwin). + report.expect(count("5StateiED") >= 1 && count("5StatelED") >= 1, + "ConstSuggestionImplicitMembers: both Holder destructors are identified"); + report.expect(count("Plain<") == 0, + "ConstSuggestionImplicitMembers: a class without a nested type gets none " + "(not identified whatever the class)"); + std::map owner; + bool apart = true; + for (const cs::UnitFacts::Identity& identity : unit.identities) + { + for (const std::string& key : + {identity.nested.empty() ? "" : "n" + identity.nested + '\0' + identity.enclosing, + identity.specialization.empty() ? "" : "s" + identity.specialization}) + { + if (key.empty()) + continue; + const auto [it, added] = owner.try_emplace(key, identity.symbol); + apart = apart && (added || it->second == identity.symbol); + } + } + report.expect(apart, "ConstSuggestionImplicitMembers: two symbols never share a key (the " + "type, the variant and the class's arguments keep them apart)"); + return report.failures == 0; + } + + /// #217 (final review of #239, M5): a plan's rewrite that cannot be carried out says so. A + /// file that can no longer be read, and one that no longer has the text the plan was made on + /// (an edit past its end), name the file and leave the result unjudged, without an exception; + /// a plan on an unchanged file rewrites it. + bool testConstSuggestionRewriteFailures(TestReport& report) + { + namespace cs = ctrace::stack::analysis::const_suggestion; + const std::filesystem::path dir = + std::filesystem::temp_directory_path() / + ("ct_const_suggestion_rewrite_" + std::to_string(::getpid())); + std::filesystem::create_directories(dir); + const std::filesystem::path file = dir / "r.c"; + std::ofstream(file) << "int read_value(int *p) { return *p; }\n"; + const auto attempt = [](const cs::SuggestionPlan& plan, std::string& failure) + { + try + { + return std::optional>( + cs::rewrite(plan, failure)); + } + catch (const std::exception& error) + { + failure = std::string("exception: ") + error.what(); + return std::optional>(); + } + }; + cs::SuggestionPlan plan; + plan.edits[file.string()] = {{15, 0, "const ", 1}}; + std::string failure; + const auto done = attempt(plan, failure); + report.expect(done && failure.empty() && + done->at(file.string()).find("const int *p") != std::string::npos, + "ConstSuggestionRewrite: an unchanged file is rewritten"); + cs::SuggestionPlan gone; + gone.edits[(dir / "missing.c").string()] = {{0, 0, "const ", 1}}; + failure.clear(); + (void)attempt(gone, failure); + report.expect(failure.find("missing.c") != std::string::npos && + failure.find("cannot be read") != std::string::npos, + "ConstSuggestionRewrite: a file that cannot be read is named"); + cs::SuggestionPlan shrunk; + shrunk.edits[file.string()] = {{4096, 3, "const ", 1}}; + failure.clear(); + (void)attempt(shrunk, failure); + report.expect( + failure.find("r.c") != std::string::npos && + failure.find("changed after the plan") != std::string::npos, + "ConstSuggestionRewrite: a file that no longer has the planned text is named, " + "without an exception"); + std::filesystem::remove_all(dir); + return report.failures == 0; + } + + /// #217 (Task 3.2, O1): the correspondences of §5.7 keep the multiplicity and the order of + /// the references at each key, those with a template and those without alike, and map the + /// instantiation contexts through the pairs that earlier rounds establish. These cases pin + /// what an index built once must give. + bool testConstSuggestionCorrespondenceIndex(TestReport& report) + { + namespace cs = ctrace::stack::analysis::const_suggestion; + const auto site = [](std::size_t offset) + { + cs::DeclarationSite out; + out.expansion = {"use.cpp", offset}; + out.spelling = out.expansion; + return out; + }; + const auto reference = [](cs::FactKey key, std::vector value) + { + cs::Fact fact; + fact.key = std::move(key); + fact.value = std::move(value); + fact.shown = fact.value.front(); + return fact; + }; + const cs::FactKey at{{"ctx"}, site(10)}; + const cs::Fact withTemplate = reference(at, {"symbol:_Z1aIiEvv", "template:a"}); + const cs::Fact rewritten = reference(at, {"symbol:_Z1aIKiEvv", "template:a"}); + const cs::Fact plain = reference(at, {"site:use.cpp:3"}); + const auto pairsOf = [&](std::vector before, std::vector after, + std::set instantiations) + { + cs::UnitFacts was; + was.entities.resize(1); + was.facts = std::move(before); + cs::UnitFacts now; + now.entities.resize(1); + now.facts = std::move(after); + now.instantiations = std::move(instantiations); + std::string missing; + std::vector out; + for (const ctrace::stack::analysis::SymbolPair& pair : + cs::correspond(was, 0, now, missing)) + out.push_back(pair.before + "->" + pair.after); + return out; + }; + using Pairs = std::vector; + report.expect(pairsOf({withTemplate, plain}, {rewritten}, {"ctx"}).empty(), + "ConstSuggestionCorrespondence: two references before, one after, at one " + "key: no pair, though only one has a template"); + report.expect(pairsOf({withTemplate, plain}, {rewritten, plain}, {"ctx"}) == + Pairs{"_Z1aIiEvv->_Z1aIKiEvv"}, + "ConstSuggestionCorrespondence: as many on both sides, in the same order: " + "the template's references pair"); + report.expect(pairsOf({withTemplate, plain}, {plain, rewritten}, {"ctx"}).empty(), + "ConstSuggestionCorrespondence: as many on both sides, in another order: " + "no pair"); + const cs::Fact outer = reference({{}, site(20)}, {"symbol:_Z1xIiEvv", "template:x"}); + const cs::Fact outerAfter = reference({{}, site(20)}, {"symbol:_Z1xIKiEvv", "template:x"}); + const cs::Fact inner = + reference({{"_Z1xIiEvv"}, site(30)}, {"symbol:_Z1yIiEvv", "template:y"}); + const cs::Fact innerAfter = + reference({{"_Z1xIKiEvv"}, site(30)}, {"symbol:_Z1yIKiEvv", "template:y"}); + report.expect(pairsOf({outer, inner}, {outerAfter, innerAfter}, {}) == + Pairs{"_Z1xIiEvv->_Z1xIKiEvv", "_Z1yIiEvv->_Z1yIKiEvv"}, + "ConstSuggestionCorrespondence: a context maps through the pair an earlier " + "round establishes"); + return report.failures == 0; + } + bool testUninitializedSummaryKeysAreLinkerSymbols(const std::filesystem::path& repoRoot, TestReport& report) { @@ -2596,6 +3388,32 @@ namespace return report.failures == 0; } + /// A symbol is recognized as mangled and shown demangled when a demangler reads it, whatever + /// the C++ runtime: one that predates a mangling, such as a C++23 explicit object member's + /// (macOS 14, GCC 11), leaves it to LLVM's (#240). A name that neither reads stays as it is. + bool testSymbolDemangling(TestReport& report) + { + using ctrace::stack::analysis::formatFunctionNameForMessage; + const std::string explicitObject = "_ZNH6Reader4peekERKS_Pi"; + const std::string shown = "Reader::peek(this Reader const&, int*)"; + report.expect(ctrace_tools::isMangled(explicitObject), + "Demangling: an explicit object member's symbol is a mangled name"); + report.expect(ctrace_tools::demangle(explicitObject.c_str()) == shown, + "Demangling: an explicit object member's symbol is shown demangled"); + report.expect(formatFunctionNameForMessage(explicitObject) == shown, + "Demangling: a message names an explicit object member demangled"); + report.expect(ctrace_tools::isMangled("_ZN6Reader4peekEPi") && + ctrace_tools::demangle("_ZN6Reader4peekEPi") == "Reader::peek(int*)", + "Demangling: a member's symbol is recognized and shown demangled"); + report.expect(!ctrace_tools::isMangled("read_value") && + formatFunctionNameForMessage("read_value") == "read_value", + "Demangling: a C function's name is no mangled name"); + report.expect(!ctrace_tools::isMangled("_Z_not_a_symbol") && + ctrace_tools::demangle("_Z_not_a_symbol") == "_Z_not_a_symbol", + "Demangling: a name that no demangler reads stays as it is"); + return report.failures == 0; + } + /// resolveParameterDebugBinding: each parameter gets its own type, and its position among the /// parameters the source declares. The others are artificial: `this`, the vtt that the /// base-object constructor (C2) of a class with a virtual base takes, is_most_derived under the @@ -2633,6 +3451,9 @@ namespace } return parameters; }; + // The arguments' bindings, in order. An expected entry written "?name: binding" is the + // parameter of an argument that only some ABIs pass: absent, or at that place with that + // binding, its IR name the parameter's or derived from it (tag.coerce). const auto check = [&](const LoadedModule& loaded, const char* symbol, const std::vector& expected) { @@ -2641,7 +3462,28 @@ namespace std::string text; for (const std::string& parameter : actual) text += " [" + parameter + "]"; - report.expect(actual == expected, + const auto passes = [](const std::string& parameter, const std::string& optional) + { + const std::size_t colon = optional.find(':'); + const std::string name = optional.substr(1, colon - 1); + return parameter == optional.substr(1) || + (parameter.starts_with(name + ".") && + parameter.substr(parameter.find(':')) == optional.substr(colon)); + }; + std::size_t next = 0; + bool matches = true; + for (const std::string& entry : expected) + { + if (entry.starts_with("?")) + { + if (next < actual.size() && passes(actual[next], entry)) + ++next; + continue; + } + matches = matches && next < actual.size() && actual[next] == entry; + ++next; + } + report.expect(matches && next == actual.size(), std::string("ParameterDebugBinding: ") + symbol + text); }; const auto load = @@ -2686,7 +3528,8 @@ namespace {".block_descriptor: artificial, source none", "x: int *, source 0", "y: double *, source 1"}); // A variadic function's debug type ends with a null entry for `...`, which no parameter - // takes; an empty parameter takes no IR argument, but keeps its debug declaration. + // takes; an empty parameter keeps its debug declaration, and takes an IR argument only on + // some ABIs (an i8 under AAPCS64 on Linux, none on x86-64 or Apple arm64). check(itanium, "_ZN7VarargsC2EPiPdz", {"this: artificial, source none", "vtt: artificial, source none", "x: int *, source 0", "y: double *, source 1"}); @@ -2695,7 +3538,7 @@ namespace "x: int *, source 0", "y: double *, source 1"}); check(itanium, "_ZN6TaggedC2EPi3Tag", {"this: artificial, source none", "vtt: artificial, source none", - "x: int *, source 0"}); + "x: int *, source 0", "?tag: Tag, source 1"}); check(itanium, "_Z8make_bigPiz", {"agg.result: artificial, source none", "p: int *, source 0"}); @@ -2710,7 +3553,7 @@ namespace // The empty parameter comes first: x is the second parameter the source declares. check(itanium, "_ZN8TagFirstC2E3TagPi", {"this: artificial, source none", "vtt: artificial, source none", - "x: int *, source 1"}); + "?tag: Tag, source 0", "x: int *, source 1"}); // Optimized code spills nothing: each argument is bound through its own debug records. check(optimized, "_ZN8VariadicC2EPiPdz", @@ -2901,6 +3744,14 @@ int main(int argc, char** argv) (void)testSarifSecuritySeverity(report); (void)testEveryRuleIsDescribed(repoRoot, report); (void)testAnalysisIncompleteNotesHaveTheirOwnRules(report); + (void)testConstSuggestionCodeComparison(report); + (void)testConstSuggestionNormalizations(report); + (void)testConstSuggestionTopLevelQualifierLost(report); + (void)testConstSuggestionCorrespondenceIndex(report); + (void)testConstSuggestionNestedCorrespondences(report); + (void)testConstSuggestionNestedIdentities(report); + (void)testConstSuggestionImplicitMemberIdentities(report); + (void)testConstSuggestionRewriteFailures(report); (void)testUnresolvedCallsMarkStackUnknown(repoRoot, report); (void)testAssumeExternalFrameReplacesUnknown(repoRoot, report); (void)testUninitializedFixpointBudgetIsExplicit(repoRoot, report); @@ -2928,6 +3779,7 @@ int main(int argc, char** argv) (void)testOwnershipCollectorExceptions(repoRoot, report); (void)testOwnershipSummaryIndex(repoRoot, report); (void)testCompileIRCacheWithoutCompilerRevision(report); + (void)testSymbolDemangling(report); if (report.failures == 0) { diff --git a/test/unit/parameter_binding_input.cpp b/test/unit/parameter_binding_input.cpp index 3551631..f2b828e 100644 --- a/test/unit/parameter_binding_input.cpp +++ b/test/unit/parameter_binding_input.cpp @@ -56,7 +56,8 @@ Varargs::Varargs(int* x, double* y, ...) seed = *x + static_cast(*y); } -// An empty parameter takes no IR argument, but keeps its debug declaration and its number. +// An empty parameter keeps its debug declaration and its number; whether it takes an IR argument +// depends on the ABI: none on x86-64 or Apple arm64, an i8 under AAPCS64 on Linux. struct Tag { };