Skip to content

[WIP][SR-14665] Warn of potentially incorrectly synthesized Equatable conformance for custom Comparable conformance #39047

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 122 commits into
base: main
Choose a base branch
from

Conversation

WowbaggersLiquidLunch
Copy link
Contributor

Currently Comparable inherits from Equatable, but does not provide a default implementation for ==, so the compiler synthesizes one composed of member-wise ==s. This leads to a problem where if a type's < is not composed of member-wise inequalities, then <, >, and == can all evaluate to false for some pairs of values, contradicting Comparable's documentation:

Types with Comparable conformance implement the less-than operator (<) and the equal-to operator (==). These two operations impose a strict total order on the values of a type, in which exactly one of the following must be true for any two values a and b:

  • a == b
  • a < b
  • b < a

For example:

struct Length: Comparable {
    enum Unit: Double, Comparable {
        case mm = 0.001
        case m = 1
        case banana = 0.178
    }
    
    let magnitude: Double
    let unit: Unit
    
    static func < (lhs: Self, rhs: Self) -> Bool {
        lhs.magnitude * lhs.unit.rawValue < rhs.magnitude * rhs.unit.rawValue
    }
}

let aBanana = Length(magnitude: 1, unit: .banana)
let oneBanana = Length(magnitude: 0.178, unit: .m)

print(aBanana < oneBanana)  // prints "false", because Length's < says so.
print(aBanana > oneBanana)  // prints "false", because Comparable's default implementation of >(a,b) is <(b,a).
print(aBanana == oneBanana) // prints "false", because the 2 Length instances are not member-wise equal.

Relevant forums discussion: https://forums.swift.org/t/add-default-implementation-of-to-comparable/48832

This bug has previously resulted in incorrect semantic version comparison in SwiftPM (swiftlang/swift-package-manager#3486 and swiftlang/swift-tools-support-core#214)


The implementation in this PR is mostly based on #27801. It checks if a Comparable conformance is user-implemented or compiler-synthesized. If it's the former, it presents a warning to the user that the synthesized == might contradict the user-defined <. Probably a better implementation would be checking if a user defined < is composed of (or equivalent to) member-wise inequalities, so it can avoid false positives. However, my C++ skills and my knowledge of the Swift compiler are rather lousy, so I'm not able to code out the better implementation.


This PR is currently a work in progress, because I need to add tests to it.

@@ -454,6 +454,43 @@ ValueDecl *DerivedConformance::deriveEquatable(ValueDecl *requirement) {

// Build the necessary decl.
if (requirement->getBaseName() == "==") {
auto NominalType = Nominal->getDeclaredType();
ASTContext &ConformanceContext = ConformanceDecl->getASTContext();
Copy link
Contributor

Choose a reason for hiding this comment

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

DerivedConformance already has a Context instance variable, you can use that (the ASTContext instance is a global singleton).

@@ -454,6 +454,43 @@ ValueDecl *DerivedConformance::deriveEquatable(ValueDecl *requirement) {

// Build the necessary decl.
if (requirement->getBaseName() == "==") {
auto NominalType = Nominal->getDeclaredType();
Copy link
Contributor

Choose a reason for hiding this comment

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

Use getDeclaredInterfaceType() instead

if (ComparableConformance) {
DeclName LessThanFunctionName(
ConformanceContext,
DeclBaseName(ConformanceContext.Id_LessThanOperator),
Copy link
Contributor

Choose a reason for hiding this comment

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

It should work to just pass the Identifier directly to getWitnessByName(). It will implicitly convert to a DeclName.

DeclBaseName(ConformanceContext.Id_LessThanOperator),
{Identifier(), Identifier()});
ConcreteDeclRef LessThanFunctionWitness =
ComparableConformance.getWitnessByName(NominalType->getRValueType(),
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need getRValueType() here.

@CodaFi
Copy link
Contributor

CodaFi commented Aug 25, 2021

How is the user meant to silence this warning if they intend for this behavior?

…d `<`

Synthesized `==` is composed of member-wise `==`s, which is likely incorrect if the non-synthesized `<` is not composed of member-wise inequalities.
@WowbaggersLiquidLunch WowbaggersLiquidLunch force-pushed the warn-synthesised-==-when-nominal-type-has-nonsynthesised-comparable branch from eed2918 to 667fe74 Compare August 28, 2021 03:24
@WowbaggersLiquidLunch
Copy link
Contributor Author

@slavapestov I made the changes that you suggested, but I'm not sure if I did them correctly (the last time I used C++ was from 4 years ago). Could you help me check them again?

@WowbaggersLiquidLunch
Copy link
Contributor Author

How is the user meant to silence this warning if they intend for this behavior?

@CodaFi I don't think the user can silence the warning with the current implementation in this PR. Something like the following code will result in a false-positive warning:

struct IntegerWrapper: Comparable {
    let wrappedInteger: Int
    func < (lhs: Self, rhs: Self) {
        lhs.wrappedInteger < rhs.wrappedInteger
    }
}

even though the code above is correct, and basically the same as what the compiler would have synthesized.

Is there a way to offer user the ability to silence this warning? I'm not familiar with things in the compiler, so any advice would be great!

I think a better solution would be for the compiler to check if the user-implemented < is composed of member-wise inequalities, and don't warn the user if it is. However, right now, I don't know how to write the code for this logic.

@WowbaggersLiquidLunch
Copy link
Contributor Author

@swift-ci please smoke test

rintaro and others added 23 commits September 20, 2021 09:12
Just for convenicence.

* Replace `llvm::isa_and_nonnull` with imported `isa_and_nonnull`
* Repalce some `EXPR && isa<T>(EXPR)` with `isa_and_nonnull<T>(EXPR)`
Allow the installation of the back-deployment shared libraries for
concurrency via build-script.
… name

When linking against the back-deployed _Concurrency library, ensure that
clients record an `@rpath`-relative dependency.
When testing the runtime in the OS (via the lit parameter
`use_os_stdlib`), add the path to the back-deployed concurrency
libraries into `DYLD_LIBRARY_PATH` for the test run. This should allow
us to run our regression tests on older OS's.
build-script-impl effectively ignores the `install_destdir` provided to
it by build-toolchain, and uses something based on the build subdir
instead. But build-script products do follow `install_destdir`, so
make `build-toolchain` provide something closer to correct.
…odel them similarly as Swift SPIs

For clang symbols marked with SPI_AVAILABLE, we add SPIAccessControlAttr to them so they will be
considered as SPIs in the AST. To be able to use all these symbols, we also add an implicit SPI import
statement for all clang modules. All clang SPIs belong to the same SPI group named "OBJC_DEFUALT_SPI_GROUP" because clang
currently doesn't support custom SPI group.

rdar://73902734
In a back deployment scenario, this will provide a place where one could provide
function implementations that are not available in the relevant stdlib.

This is just setting up for future work and isn't doing anything interesting
beyond wiring it up/making sure that it is wired up correctly with tests.
Remove the option that explicitly enables concurrency back-deployment,
and instead always enable its support in the compiler. Remove the use
of the extraneous CMake option as well.
…locations.

Those instructions should not be "visible" for debugging and for diagnostic messages.

Fixes a wrong "unreachable code" warning.

https://bugs.swift.org/browse/SR-14873
rdar://80237870
There can be, currently, up to eight child nodes for a FunctionType.
OldRemangler seemed to think there could only be three, while NodePrinter
plumped for six.

rdar://82252704
It is not well supported

rdar://82082527
Once coerced initializer expression has been set for
a particular pattern binding entry, let's mark it as
checked, otherwise decl checker might try to
re-typecheck it.
Coercion should be aligned with `PatternBindingEntryRequest::evaluate`
and that uses only one flag - `TypeResolverContext::PatternBindingDecl`.
slavapestov and others added 28 commits September 20, 2021 09:12
If you have a class with an (invalid) protocol nested inside:

    class OuterGenericClass<T, U> {
      protocol InnerProtocol : OuterGenericClass {}
    }

The reference to 'OuterGenericClass' in the protocol's inheritance
clause is not "in context", and should not have inferred generic
arguments <T, U>, because the protocol does not inherit the generic
arguments of the outer scope.

Previously, this would trigger an assertion in the requirement
machine, because the generic parameter τ_0_1 is not a valid generic
parameter in the protocol's generic signature.
Darwin OSes support vouchers, which are key/value sets that can be adopted on a thread to influence its execution, or sent to another process. APIs like Dispatch propagate vouchers to worker threads when running async code. This change makes Swift Concurrency do the same.

The change consists of a few different parts:

1. A set of shims (in VoucherShims.h) which provides declarations for the necessary calls when they're not available from the SDK, and stub implementations for non-Darwin platforms.
2. One of Job's reserved fields is now used to store the voucher associated with a job.
3. Jobs grab the current thread's voucher when they're created.
4. A VoucherManager class manages adoption of vouchers when running a Job, and replacing vouchers in suspended tasks.
5. A VoucherManager instance is maintained in ExecutionTrackingInfo, and is updated as necessary throughout a Job/Task's lifecycle.

rdar://76080222
Otherwise, these tests fail when one runs these tests against a just
built toolchain but have not yet installed a stage2 stdlib.
And restructure the "Debug Information" section to make it flow better.
If anyone else is building Windows ARM64 they should be using a new
enough Visual Studio.  This workaround is more difficult to keep working
properly and the CI hosts should have a new enough Visual Studio
installation hopefully in order to enable the ARM64 builds of the
runtime.  If they do not, we can re-evaluate whether to re-instate the
workaround.  This allows building part of the runtime with Visual Studio
2022 and reduces the maintenance overheads for the runtime.
All enum elments should have been handled separately so we don't need to anything special
for EnumCaseDecl except consuming them.
…` since it's not needed there

Also allows removing the "swift/Runtime/Config.h" include from that header.
Returning a null GenericSignature is not the right way to break a cycle,
because then callers have to be careful to handle the case of a null
GenericSignature together with a non-null GenericParamList, for example
in applyGenericArguments().

An even worse problem can occur when a GenericSignatureRequest for a
nested generic declaration requests the signature of the parent context,
which hits a cycle. In this case, we would build a signature where
the first generic parameter did not have depth 0.

This makes the requirement machine upset, so this patch implements a new
strategy to break such cycles. Instead of returning a null
GenericSignature, we build a signature with the correct generic
parameters, but no requirements. The generic parameters can be computed
just by traversing GenericParamLists, which does not trigger more
GenericSignatureRequests, so this should be safe.
Introduce the ArgumentList type, which represents
a set of call arguments for a function or
subscript. This will supersede the use of tuple
and paren exprs as argument lists.
Switch out the representation of argument lists
in various AST nodes with ArgumentList.
Split up the expr list parsing members such that
there are separate entry points for tuple and
argument list parsing, and start using the argument
list parsing member for call and subscripts.
With the removal of TupleExpr and ParenExpr argument
lists, it's no longer sufficient to check whether
a given parent expr has a possible chain sub-expr,
as it might be a child that's unrelated to the chain
under consideration. For example, for a chain that's
an argument to a function call, we don't want to
walk up and consider the call expr to be an extension
of the chain just because it has a function expr.

Tighten up the check by comparing whether the given
sub-expr of a possible chain parent matches the
current expr in the chain.
- Explicitly limit favoring logic to only handle
unary args, this seems to have always been the
case, but needs to be handled explicitly now that
argument lists aren't exprs
- Update the ConstraintLocator simplification to
handle argument lists
- Store a mapping of locators to argument lists
in the constraint system
- Abstract more logic into a getArgumentLocator
method which retrieves an argument-to-param locator
from an argument anchor expr
Most of this should be fairly mechanical, the
changes in PlaygroundTransform are a little more
involved though.
Track the depth and clear out the map when it
reaches 0 to save memory.
We were accidentally continuing to walk the old
argument list even if the caller had returned a
new one.
- Remove OriginalArguments in favor of storing the
pre-rewritten argument list, which simplifies things
nicely
- Adopt llvm::indexed_accessor_iterator
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.