Skip to content

Fix handling of functions with multiple definitions#3

Open
eduard-bagdasaryan wants to merge 10 commits intomainfrom
SQUID-1100-fix-handling-functions-with-multiple-definitions
Open

Fix handling of functions with multiple definitions#3
eduard-bagdasaryan wants to merge 10 commits intomainfrom
SQUID-1100-fix-handling-functions-with-multiple-definitions

Conversation

@eduard-bagdasaryan
Copy link

@eduard-bagdasaryan eduard-bagdasaryan commented Nov 3, 2025

FunctionDeclMatchHandler::finalize(), working at a single TU at a time,
accumulates AllDecls entries using two loops:

  • The first loop handles functions that are defined but unused in TU.
  • The second loop handles functions that are used but not defined in TU.

A function "use" case was incorrectly ignored (i.e. the function was
not marked as used in AllDecls) when every TU using that function also
contained that function definition -- neither loop covered this case. If
some other TU defined that function without using it, the function would
be incorrectly reported as unused.

For example, a test program containing a foo() stub definition would
trigger an incorrect "foo() is unused" warning if the primary project
program was defining and using foo() in some TU while the test program
was not using foo() at all.

Instead of reporting all unused functions, xunused only reports unused
functions that are defined by the project. Ignoring undefined functions
is probably necessary to avoid reporting "externally" defined (e.g., by
a 3rd party library) functions as unused by the project. This change
preserves that approach, but stops using "each function can be defined
at most once" assumption (that could be appropriate for projects with a
single executable) to add support for projects where multiple
executables provide multiple definitions for the same function.

The two finalize() loops now have the following simplified scope:

  • The first loop handles functions that are defined in TU.
  • The second loop handles functions that are used in TU.

FunctionDeclMatchHandler::finalize(), working at a single TU at a time,
accumulates dependencies in two separate 'Uses' 'Defs' sets:

* Uses: All functions that are used but not defined in the TU
* Defs: All functions that are defined but not used in the TU

This lacks the case when a function is defined _and_ used in the TU
at the same time. Such function was omitted and could later be
considered as 'unused', e.g., if another definition was found
in a tests 'stub' file.
Copy link

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FunctionDeclMatchHandler::finalize(), working at a single TU at a time

I agree that finalize() is called once for each TU.

accumulates dependencies in two separate 'Uses' 'Defs' sets:
* Uses: All functions that are used but not defined in the TU
* Defs: All functions that are defined but not used in the TU

My reading of this code is very different!

finalize() does not accumulate something in Uses and Defs. finalize() does not change Uses and Defs at all (except in isWeak() corner case that this PR removes).

Instead, Uses is built by handleUse(), and Defs is built by run(). Neither handleUse() nor run() are called once for each TU. They are called as various matching AST nodes are found.

When finalize() iterates Defs and Uses, it does not look at functions from a single TU. It looks at functions from all already processed/seen TUs and the current TU.

finalize() populates and updates AllDecls.

finalize() loops adds the following AllDecls entries:

  • The first loop adds functions that are defined but unused in seen TUs.
  • The second loop adds functions that are used but not defined in seen TUs.

... where "seen TUs" are all TUs from previous finalize() calls and the current TU.

This lacks the case when a function is defined and used in the TU at the same time. Such function was omitted and could later be considered as 'unused', e.g., if another definition was found in a tests 'stub' file.

I more-or-less agree with this part. To be more precise, a function "use" was incorrectly skipped (i.e. the function was not added to AllDecls as used) when for every TU using that function, a seen TU contained that function definition. If some initial seen TUs sequence defined the function without using it, the function would be incorrectly reported as unused in such cases.

If I am right, then the bug we are trying to fix here is not specific to functions with multiple definitions. A single definition can trigger the same problem.

Please merge main branch and add the simplest test case (or two) that can trigger this problem. Alternatively, if you prefer, close this PR and start from scratch, keeping this review in mind.

As always, please keep PR title/description in sync with PR code (and with our understanding of the proposed changes).

main.cpp Outdated

// TODO: use in Uses and Defs sets to exclude duplicates
// (i.e., when multiple FunctionDecl reference a single object)
struct FunctionDeclPtrLess {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FunctionDeclPtrLess class is unused by current PR code. If it is not required to fix the problem, let's not add it (in this PR).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

main.cpp Outdated

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR code cannot claim that this USR is unused. I suggest removing this debugging/comment and adding a new debugging/comment line to the first if statement below, with found new definition: or similar text.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

main.cpp Outdated

for (auto *F : ExternalUses) {
for (auto *F : Uses) {
// llvm::errs() << "ExternalUses: " << F->getNameAsString() << "\n";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR code cannot claim that F represents an external use. I suggest removing this debugging/comment and adding a new debugging/comment line after the second if statement below, with saw ... uses: or similar text that will also report it_inserted.first->second.Uses counter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

main.cpp Outdated

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern here. Same recommended solution (i.e. replace both stale debugging/comments with one fresh debugging/comment line further below).

main.cpp Outdated
XUnusedASTConsumer() {
Matcher.addMatcher(
functionDecl(isDefinition(), unless(isImplicit())).bind("fnDecl"),
functionDecl(unless(isImplicit())).bind("fnDecl"),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to remove isDefinition() here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isDefinition() is false when a TU has a function declaration without definition. Generally, as far as we are concerned to cover all function's occurrences, I think we should not use this filter. However, this change looks unnecessary in this particular PR and I will remove it.

main.cpp Outdated

struct DefInfo {
const FunctionDecl *Definition;
const FunctionDecl *Definition; // XXX: perhaps a dangling pointer (created during parsing a single TU)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that FunctionDecl objects survive across TU boundaries? If we are not sure, and we do not really care/dereference this Definition pointer in this PR, I suggest removing this scary comment from this PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that FunctionDecl objects survive across TU boundaries?

I think it is not possible.

removing this scary comment

Ok.

main.cpp Outdated
Comment on lines 116 to 119
@@ -107,14 +117,9 @@ class FunctionDeclMatchHandler : public MatchFinder::MatchCallback {
// Defs before checking which uses we need to consider in other TUs,
// so the functions overwritting the weak definition here are marked
// as used.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need to remove discard_if() below, then we should remove this comment as well, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

@eduard-bagdasaryan
Copy link
Author

eduard-bagdasaryan commented Nov 17, 2025

finalize() does not accumulate something in Uses and Defs

Agreed. I think there was a mistake in the description: s/Uses/ExternalUses and s/Defs/UnusedDefs. Probably this happened because in the PR I replaced ExternalUses with Uses and UnusedDefs with Defs.

Uses is built by handleUse(), and Defs is built by run(). Neither handleUse() nor run() are called once for each TU.

Agreed, run() is a callback used by Matcher.matchAST(). But XUnusedASTConsumer is created for each parsed TU (and hence FunctionDeclMatchHandler containing Uses and Defs).

When finalize() iterates Defs and Uses, it does not look at functions from a single TU. It looks at functions from all already processed/seen TUs and the current TU.

Agreed in the sense that Uses and Defs represent functions from a single TU and AllDecls represent already processed TUs.

Copy link
Author

@eduard-bagdasaryan eduard-bagdasaryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done up to 64204aa.

main.cpp Outdated

struct DefInfo {
const FunctionDecl *Definition;
const FunctionDecl *Definition; // XXX: perhaps a dangling pointer (created during parsing a single TU)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that FunctionDecl objects survive across TU boundaries?

I think it is not possible.

removing this scary comment

Ok.

main.cpp Outdated

// TODO: use in Uses and Defs sets to exclude duplicates
// (i.e., when multiple FunctionDecl reference a single object)
struct FunctionDeclPtrLess {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

main.cpp Outdated
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

main.cpp Outdated
Comment on lines 116 to 119
@@ -107,14 +117,9 @@ class FunctionDeclMatchHandler : public MatchFinder::MatchCallback {
// Defs before checking which uses we need to consider in other TUs,
// so the functions overwritting the weak definition here are marked
// as used.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

main.cpp Outdated

for (auto *F : ExternalUses) {
for (auto *F : Uses) {
// llvm::errs() << "ExternalUses: " << F->getNameAsString() << "\n";
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

main.cpp Outdated
XUnusedASTConsumer() {
Matcher.addMatcher(
functionDecl(isDefinition(), unless(isImplicit())).bind("fnDecl"),
functionDecl(unless(isImplicit())).bind("fnDecl"),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isDefinition() is false when a TU has a function declaration without definition. Generally, as far as we are concerned to cover all function's occurrences, I think we should not use this filter. However, this change looks unnecessary in this particular PR and I will remove it.

Copy link

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alex: When finalize() iterates Defs and Uses, it does not look at functions from a single TU. It looks at functions from all already processed/seen TUs and the current TU.

Eduard: Agreed in the sense that Uses and Defs represent functions from a single TU

Agreed. I got Uses and Defs lifetime/scope wrong because I missed the fact that XUnusedFrontendActionFactory apparently creates a new XUnusedASTConsumer object for what xunused comment calls "each source file" (i.e. presumably each TU), creating a new XUnusedASTConsumer::Handler data member that holds Uses and Defs. This correction is good news because it means that current official code is closer to what it should be.

I adjusted PR description accordingly.

@rousskov
Copy link

Alex: I adjusted PR description accordingly.

I adjusted the last PR description paragraphs based on our discussions.

main.cpp Outdated

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether this PR would be the right place to change the Definition data member from a FunctionDecl pointer to a Definitions counter (similar to the existing Uses counter). Prior to this PR, only a single definition was allowed/supported, so a pointer to the corresponding declaration was appropriate (even if it was never dereferenced). After this PR, we allow multiple definitions, so a single pointer does not really make sense.

Please implement that change as the last dedicated branch commit (after fixing all other issues as discussed) so that we can easily revert it if necessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would be useful. I also adjusted debugging to report this counter.

main.cpp Outdated

for (auto *F : ExternalUses) {
// llvm::errs() << "ExternalUses: " << F->getNameAsString() << "\n";
for (auto *F : Uses) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible:

Suggested change
for (auto *F : Uses) {
for (const auto F: Uses) {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@eduard-bagdasaryan
Copy link
Author

adjusted the last PR description paragraphs

Checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants