Skip to content

C#: Extract and use ambiguous type information for call target resolution #14891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

tamasvajk
Copy link
Contributor

@tamasvajk tamasvajk commented Nov 23, 2023

This PR

  • extracts
    • candidate type symbols reported by Roslyn when facing ambiguous types
    • invoked method names
  • modifies MethodCall::getTarget to look up candidate call targets when the qualifier of the call has an ambiguous type.

@github-actions github-actions bot added the C# label Nov 23, 2023
@tamasvajk tamasvajk force-pushed the standalone/ambiguousTypes branch from f7cac13 to f21f16a Compare November 28, 2023 10:23
@tamasvajk tamasvajk changed the title C#: Add test case for ambiguous types in Standalone extraction C#: Extract and use ambiguous type information for call target resolution Dec 1, 2023
@tamasvajk
Copy link
Contributor Author

@hvitved, @michaelnebel Could you have a look at this PR? It's not exactly ready for review. DCA is showing significant slowdown. But I'd like to get some early feedback on the approach, which significantly changes how we come up with call targets.

{
if (memberName is not null)
{
trapFile.invocation_member_name(this, memberName);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are all dynamic calls excluded?
A far as I understand, a call can be considered dynamic, if just one of the arguments are.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good point. We could include dynamic_member_name in MethodCall::getACandidateTarget, where we use invocation_member_name. But there's no need to store dynamic member names duplicately in the DB.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've done this in 82c675a.

not this.hasMultipleParamsArguments()
not this.hasMultipleParamsArguments(c)
or
result.getType().isImplicitlyConvertibleTo(p.getType().(ArrayType).getElementType())
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think getArgumentForParameter was not handling correctly params before. The corresponding tests also report more argument-parameter pairs now. See here.

In case of params, we were already checking the types with isValidExplicitParamsType, which I think checks whether an array type is convertible to another array type, such as string[] to object[]. For the expanded form, we'd need to check the types agains the element type of the array.

void M1(string s, params int[] a) {}
void M2(string s, params double[] a) {}

M1("", 1, 2, 3);         // Viable
M1("", 1.1, 2.2, 3.3);   // This is not a viable call to M due to `1.1` having `double` as a type, which is not convertible to the element type `int` of array type `int[]`.

M2("", 1, 2, 3);         // Viable, `int` is implicitly convertible to `double`. 
M2("", 1.1, 2.2, 3.3);   // Viable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hvitved raised the point below that isImplicitlyConvertibleTo might not work for generics. This is something that we should check and fix here. final override Expr getArgumentForParameter should work for traced DBs.

this.getQualifier()
.getType()
.(ValueOrRefType)
.getABaseType*()
Copy link
Contributor

Choose a reason for hiding this comment

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

Including base types here somehow seems related to dispatching. Could this be a potential performance killer? Would it make sense to initially only look at type alternatives without taking base types into account?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test cases show that including the base classes is needed. At the same time, it can easily be a performance killer.

.getType()
.(ValueOrRefType)
.getABaseType*()
.getAnAmbiguousAlternativeType*() and
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens, if we only look at "one" level of alternatives? Does that cover most cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've done this in a4eb47f

@michaelnebel
Copy link
Contributor

I think this looks really interesting.
Have you found the source of the performance issues?

@tamasvajk
Copy link
Contributor Author

@hvitved Have you got a chance to do an early review of this PR?

Copy link
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

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

Some initial comments.

@@ -45,6 +45,23 @@ class Type extends DotNet::Type, Member, TypeContainer, @type {

/** Holds if this type is a value type, or a type parameter that is a value type. */
predicate isValueType() { none() }

/** Gets an alternative type that is reported by the compiler as being ambiguous with this type. */
Type getAnAmbiguousAlternativeType() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Modeling it in this way means that we cannot distinguish when a type is used unambiguously and when it is used ambiguously, that may be too simplistic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't not exists(this.getAnAmbiguousAlternativeType()) mean that the type is used unambiguously?

Copy link
Contributor

Choose a reason for hiding this comment

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

I may have misunderstood how it works, but consider this example:

namespace N1 {
    class C {}
}

namespace N2 {
    class C {}
}

namespace N3 {
    class C {}
}

namespace N4 {
    using N1;
    using N2;
    
    class C3 {
        C Field; // either `N1.C` or `N2.C`, not `N3.C`
    }
}

namespace N5 {
    using N1;
    using N3;
    
    class C3 {
        C Field; // either `N1.C` or `N3.C`, not `N2.C`
    }
}

AFAICT, N1.C, N2.C, and N3.C will be considered equal modulo getAnAmbiguousAlternativeType, but that is an over approximation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, your understanding is correct, we over approximate. What would be your suggestion? (Let's continue this discussion on Slack)

Comment on lines 347 to 291
not p.isParams() and
arg.getType().isImplicitlyConvertibleTo(p.getType())
or
p.isParams() and
arg.getType().isImplicitlyConvertibleTo(p.getType().(ArrayType).getElementType())
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this works if either the parameter type or the argument type contains type parameters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can definitely improve this further. At the same time, I don't think it's critical. It means that there are cases when we don't find a candidate call target, doesn't it? The opposite can also happen: we may find too many candidate call targets. These are expected as we haven't implemented the entire resolution logic.

@tamasvajk tamasvajk force-pushed the standalone/ambiguousTypes branch 2 times, most recently from 527ce26 to 815c2ec Compare January 11, 2024 09:56
@tamasvajk tamasvajk force-pushed the standalone/ambiguousTypes branch from 2c65eba to b67f8f3 Compare January 14, 2024 14:02
@tamasvajk tamasvajk force-pushed the standalone/ambiguousTypes branch from d35fc33 to 52aa355 Compare January 18, 2024 07:54
@tamasvajk
Copy link
Contributor Author

DCA performance looks good. Removed/added alert count doesn't seem as good as I expected. We do find 9 new cs/hardcoded-credentials in aspnetboilerplate/aspnetboilerplate which was prompting this change in the first place. At the same time, we only find 3 additional alerts and we lose 51 in the test suite. I'll need to check if the removed 51 were false positives or not...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants