Skip to content

[Constraint solver] Improve fix behaviors for better concurrency-related recovery and overloading #59958

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

Merged

Conversation

DougGregor
Copy link
Member

@DougGregor DougGregor commented Jul 8, 2022

Improve the modeling of fix behaviors in the constraint solver rather than abusing DiagnosticBehavior for this purpose. Clearly call out the various cases (error, always-warning, downgrade-to-warning, ignore) and separate the predicates for "prevents solution application" and "should impact the score."

With that refactoring in place, tune the behavior for recoverable concurrency fixes to (1) apply even when we aren't allowing fixes (i.e., along the fast path), which improves solver performance overall when dealing with concurrency issues like a missing @Sendable, and (2) classify recoverable errors as "disfavored overloads" so that are more desirable than outright errors, which addresses the overloading problem from #59909.

Rather than re-using `DiagnosticBehavior` to describe how a fix should
act, introduce `FixBehavior` to cover the differences between (e.g.)
always-as-awarning and downgrade-to-warning. While here, split the
`isWarning` predicate into two different predicates:

* `canApplySolution`: Whether we can still apply a solution when it
contains this particular fix.
* `affectsSolutionScore`: Whether

These two predicates are currently tied together, because that's the
existing behavior, but we don't necessarily want them to stay that way.
This allows us to still maintain them in the score kind, but not treat
them as being as severe as an error requiring a fix.
This avoids us having to go down the less-efficient "salvage" path when
dealing with concurrency issues. It also fixes overloading behavior
when dealing with `@preconcurrency` and `@Sendable` functions,
such as in swiftlang#59909.
@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor DougGregor changed the title Constraint solver recoverable error [Constraint solver] Improve fix behaviors for better concurrency-related recovery and overloading Jul 8, 2022
@DougGregor
Copy link
Member Author

@swift-ci please smoke test

Copy link
Contributor

@xedin xedin left a comment

Choose a reason for hiding this comment

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

Thank you! This definitely looks more like how I imagine it should work. I left some comments inline with some suggestions and questions.

/// The diagnostic behavior limit that will be applied to any emitted
/// diagnostics.
DiagnosticBehavior diagBehaviorLimit() const { return behaviorLimit; }
FixBehavior diagfixBehavior() const { return fixBehavior; }
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it might make sense to turn this into a limitsDiagnosticBehaviorTo() or something like that which returns Optional<DiagnosticBehavior> so that we could just supply it for to FailureDiagnostic and avoid having to mix fixes and diagnostics.

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to keep around the FixBehavior for when we create a FailureDiagnostic from a ConstraintFix, which is why I factored this out.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please see my comment about this attached to FailureDiagnostic constructor.

namespace constraints {

/// Describes the behavior of the diagnostic corresponding to a given fix.
enum class FixBehavior {
Copy link
Contributor

Choose a reason for hiding this comment

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

See my comment about limitsDiagnosticBehaviorTo, I think this could be turned into an inner enum of ConstraintFix.

for (const auto fix : solution.Fixes) {
if (!fix->canApplySolution())
canApplySolution = false;
if (fix->affectsSolutionScore() == SK_Fix && fix->canApplySolution())
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems odd to me because anything which sets SK_Fix should imply "fatal"...

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, this change became unnecessary with my last commit, because we no longer increase SK_Fix for anything recoverable. I can remove it now.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, but I've been thinking about adding a RecoverableError to FixBehavior, which is an error from which we can still recover. We still want it to bump SK_Fix so there's no difference from Error in the solver behavior, but we can still produce a type-checked AST, which helps downstream code. Adding @Sendable in an argument position is a case where this would work, because CSApply will just toss in another function conversion expression.

Copy link
Contributor

Choose a reason for hiding this comment

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

It sounds like we need to split SK_Fix into SK_FatalFix and SK_RecoverableFIx or something like that.

*this, func1, func2, getConstraintLocator(locator),
getConcurrencyDiagnosticBehavior(true));
if (recordFix(fix))
if (auto fixBehavior = getConcurrencyFixBehavior(true)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We have a pattern for this kind of a situation - a fix declares a static attempt method which returns a fix pointer. This way getConcurrencyFixBehavior could be turned into a implementation detail of AddSendableAttribute and MarkGlobalActorFunction and moved to CSFix.cpp and the code here becomes:

if (auto fix = AddSendableAttribute::attempt(*this, func1, func2, getConstraintLocator(locator))) {
  if (record(fix))
    return getTypeMatchFailure(locator);
} else {
    return getTypeMatchFailure(locator);
}

DiagnosticBehavior behaviorLimit =
DiagnosticBehavior::Unspecified)
: S(solution), Locator(locator), behaviorLimit(behaviorLimit) {}
FixBehavior fixBehavior = FixBehavior::Error)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can avoid mixing fixes and diagnostics here. How about we bring back behaviorLimit but make it Optional<DiagnosticBehavior> which is going to be None by default, and it's going to be supplied by the fix that constructs a diagnostic via call to limitsDiagnosticBehaviorTo()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because we want isFatal and impact to operate on FixBehavior, we need to carry FixBehavior through here and map it down to DiagnosticBehavior only at the point of diagnostic emission.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not exactly sure what you mean - FailureDiagnostic represents a diagnostic emission for a ConstraintFix point of view, so the fix should tell it whether it wants to limit the behavior or not, it shouldn't be up to diagnostic to decide what to do based on the fix behavior that's why I suggested to package that logic into ConstraintFix::limitsDiagnosticBehaviorTo() which is going to produce Optional< DiagnosticBehavior>.

@DougGregor
Copy link
Member Author

Thank you for the review @xedin ! I agree with most of your feedback and will follow-up with a cleanup PR to address it.

@DougGregor DougGregor merged commit 5b537ea into swiftlang:main Jul 8, 2022
@DougGregor DougGregor deleted the constraint-solver-recoverable-error branch July 8, 2022 16:51
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