Skip to content

Sema: Support optional promotion of tuple patterns #63992

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -4635,8 +4635,9 @@ WARNING(found_one_pattern_for_several_associated_values,Deprecation,
WARNING(converting_tuple_into_several_associated_values,none,
"enum case '%0' has %1 associated values", (StringRef, unsigned))
WARNING(converting_several_associated_values_into_tuple,none,
"enum case '%0' has one associated value that is a tuple of %1 "
"elements",(StringRef, unsigned))
"enum case '%0' has one associated value that is a %select{|optional }1"
"tuple of %2 elements",
(StringRef, bool, unsigned))
ERROR(closure_argument_list_tuple,none,
"contextual closure type %0 expects %1 argument%s1, "
"but %2 %select{were|was}3 used in closure body", (Type, unsigned, unsigned, bool))
Expand Down
6 changes: 6 additions & 0 deletions include/swift/Sema/Constraint.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ enum class ConstraintKind : char {
/// The first type is an optional type whose object type is the second
/// type, preserving lvalue-ness.
OptionalObject,
/// The first type is an optional of non-negative depth of the second type,
/// ignoring lvalueness (because the constraint is currently applied only to
/// pattern types, which are always rvalue). For example, given a RHS of
/// 'Int', a LHS of 'Int' or 'Int??' would satisfy this constraint.
EqualOrOptional,
Comment on lines +161 to +165
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't quite feel right to me to have Equal in the name since the constraint isn't doing rvalue conversions, it's just asserting that no lvalues are present. Additionally, I think if we did ever want to apply this constraint to something else that could have lvalues, I think it would probably make most sense to preserve lvalueness like OptionalObject, as lvalues are generally preserved through optional chains.

How about something like UnwrappingOptionals? I think the plurality also helps make it clear it unwraps an arbitrary number of optionals.

/// The first type is the same function type as the second type, but
/// made @escaping.
EscapableFunctionOf,
Expand Down Expand Up @@ -696,6 +701,7 @@ class Constraint final : public llvm::ilist_node<Constraint>,
case ConstraintKind::DynamicCallableApplicableFunction:
case ConstraintKind::BindOverload:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::OneWayEqual:
case ConstraintKind::OneWayBindParam:
case ConstraintKind::DefaultClosureType:
Expand Down
6 changes: 6 additions & 0 deletions include/swift/Sema/ConstraintSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -4693,6 +4693,12 @@ class ConstraintSystem {
TypeMatchOptions flags,
ConstraintLocatorBuilder locator);

/// Attempt to simplify an equal-or-optional constraint.
SolutionKind
simplifyEqualOrOptionalConstraint(Type first, Type second,
TypeMatchOptions flags,
ConstraintLocatorBuilder locator);

/// Attempt to simplify the BridgingConversion constraint.
SolutionKind simplifyBridgingConstraint(Type type1,
Type type2,
Expand Down
1 change: 1 addition & 0 deletions lib/Sema/CSBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,7 @@ void PotentialBindings::infer(Constraint *constraint) {
case ConstraintKind::ArgumentConversion:
case ConstraintKind::OperatorArgumentConversion:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::UnresolvedMemberChainBase: {
auto binding = inferFromRelational(constraint);
if (!binding)
Expand Down
19 changes: 18 additions & 1 deletion lib/Sema/CSGen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2710,7 +2710,24 @@ namespace {
tupleTypeElts.push_back(TupleTypeElt(eltTy, tupleElt.getLabel()));
}

return setType(TupleType::get(tupleTypeElts, CS.getASTContext()));
Type patternTy = TupleType::get(tupleTypeElts, CS.getASTContext());

// 1. Allow optional promotion only when the matching is conditional,
// i.e., not in pattern binding declarations.
// 2. A single-element tuple can only mean that we are matching the
// payload of an enum case without splatting, in which case optional
// promotion is irrelevant.
if (!patternBinding && tupleTypeElts.size() > 1) {
Type tyVar = CS.createTypeVariable(CS.getConstraintLocator(locator),
TVO_CanBindToNoEscape);
CS.addConstraint(
ConstraintKind::EqualOrOptional, tyVar, patternTy,
locator.withPathElement(LocatorPathElt::PatternMatch(pattern)));
Copy link
Contributor

@xedin xedin May 1, 2023

Choose a reason for hiding this comment

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

As far as I understand this problem, what we really want is not to look through optionals but disregard the structure of the tuple pattern and let context impose it. So how about instead of this new constraint we actually model it this way:

  1. Create a "tuple" pattern type variable (just like you did);
  2. For each pattern in the tuple create a member constraint to i.e. <tuple var>[member: .<index or name>] == <element-var>;
  3. Set tuple variable as a type of the tuple pattern.

This scheme should make it possible to infer tuple type from context and look through optionals in pattern matching context while resolving individual tuple elements.

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the late reply here! I'm curious what the benefit of that approach would be, as wouldn't the new tuple member constraint need to handle the same optional unwrapping that the EqualOrOptional constraint currently handles, plus it would need to have tuple matching logic? Or is there something I'm missing here?

Or is the aim to have the <tuple var> be inferred as a non-optional? I should note that the constraint which attaches the tuple var to the surrounding pattern would fail with that, as it should only allow conversions that propagate into pattern, not out of them (though I'm becoming increasingly of the opinion that we should limit those to equality constraints where we can).

Copy link
Contributor

Choose a reason for hiding this comment

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

There are no new constraints and existing member already handles unwrapping of optional base type.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see! Sorry, I totally missed that. I guess one worry with that approach is we don't want to allow e.g (x: Int, y: Int) to match with (y: Int, x: Int), or even worse some nominal type T that just so happens to have the right members.

One other thing that we have to contend with is that we currently can use the tuple type of the pattern to introduce conversions, e.g this is currently allowed:

if case (x: 0, y: 0) = (0, 0) {}

Also just discovered that we currently allow duplicate labels in tuple patterns :(

if case (x: 0, x: 0) = (0, 0) {}

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 it should be too hard to maintain the structural matching, we can even add additional information about index/label to member constraint to make sure that the base tuple does indeed match structurally.


patternTy = tyVar;
}

return setType(patternTy);
}

case PatternKind::OptionalSome: {
Expand Down
68 changes: 67 additions & 1 deletion lib/Sema/CSSimplify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,7 @@ ConstraintSystem::matchTupleTypes(TupleType *tuple1, TupleType *tuple2,
case ConstraintKind::KeyPathApplication:
case ConstraintKind::LiteralConformsTo:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::SelfObjectOfProtocol:
case ConstraintKind::UnresolvedValueMember:
case ConstraintKind::ValueMember:
Expand Down Expand Up @@ -2619,6 +2620,7 @@ static bool matchFunctionRepresentations(FunctionType::ExtInfo einfo1,
case ConstraintKind::KeyPathApplication:
case ConstraintKind::LiteralConformsTo:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::SelfObjectOfProtocol:
case ConstraintKind::UnresolvedValueMember:
case ConstraintKind::ValueMember:
Expand Down Expand Up @@ -3126,6 +3128,7 @@ ConstraintSystem::matchFunctionTypes(FunctionType *func1, FunctionType *func2,
case ConstraintKind::KeyPathApplication:
case ConstraintKind::LiteralConformsTo:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::SelfObjectOfProtocol:
case ConstraintKind::UnresolvedValueMember:
case ConstraintKind::ValueMember:
Expand Down Expand Up @@ -6732,6 +6735,7 @@ ConstraintSystem::matchTypes(Type type1, Type type2, ConstraintKind kind,
case ConstraintKind::KeyPathApplication:
case ConstraintKind::LiteralConformsTo:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::SelfObjectOfProtocol:
case ConstraintKind::UnresolvedValueMember:
case ConstraintKind::ValueMember:
Expand Down Expand Up @@ -8873,6 +8877,60 @@ ConstraintSystem::simplifyOptionalObjectConstraint(
return SolutionKind::Solved;
}

ConstraintSystem::SolutionKind
ConstraintSystem::simplifyEqualOrOptionalConstraint(
Type first, Type second, TypeMatchOptions flags,
ConstraintLocatorBuilder locator) {
// Reify all the type variables we can.
first = getFixedTypeRecursive(first, flags, /*wantRValue=*/false);
second = getFixedTypeRecursive(second, flags, /*wantRValue=*/false);

assert(!first->hasLValueType() && !second->hasLValueType() &&
"Unexpected lvalue; constraint operand is not a pattern type?");

if (first->isEqual(second)) {
return SolutionKind::Solved;
}

// Unwrap both types simultaneously until one of them is no longer optional.
while (true) {
if (auto firstObject = first->getOptionalObjectType()) {
if (auto secondObject = second->getOptionalObjectType()) {
first = firstObject;
second = secondObject;
continue;
}
}

break;
}

const Type firstUnwrapped = first->lookThroughAllOptionalTypes();

// At this point at most one of the two types is a known optional. The
// constraint can be reduced to an equation between the fully unwrapped LHS
// and the RHS — and thus solved — if the following conditions are met:
// 1. The fully unwrapped LHS is not a type variable.
// 2. Either the LHS is not optional to begin with, or the RHS is not a
// type variable.

if (firstUnwrapped->isTypeVariableOrMember() ||
(first->isOptional() && second->isTypeVariableOrMember())) {
if (!flags.contains(TMF_GenerateConstraints)) {
return SolutionKind::Unsolved;
}

addUnsolvedConstraint(
Constraint::create(*this, ConstraintKind::EqualOrOptional, first,
second, getConstraintLocator(locator)));
return SolutionKind::Solved;
}

addConstraint(ConstraintKind::Bind, firstUnwrapped, second, locator);

return SolutionKind::Solved;
}

ConstraintSystem::SolutionKind
ConstraintSystem::simplifyBindTupleOfFunctionParamsConstraint(
Type first, Type second, TypeMatchOptions flags,
Expand Down Expand Up @@ -14528,6 +14586,9 @@ ConstraintSystem::addConstraintImpl(ConstraintKind kind, Type first,
case ConstraintKind::OptionalObject:
return simplifyOptionalObjectConstraint(first, second, subflags, locator);

case ConstraintKind::EqualOrOptional:
return simplifyEqualOrOptionalConstraint(first, second, subflags, locator);

case ConstraintKind::Defaultable:
return simplifyDefaultableConstraint(first, second, subflags, locator);

Expand Down Expand Up @@ -15074,7 +15135,12 @@ ConstraintSystem::simplifyConstraint(const Constraint &constraint) {
constraint.getSecondType(),
/*flags*/ None,
constraint.getLocator());


case ConstraintKind::EqualOrOptional:
return simplifyEqualOrOptionalConstraint(
constraint.getFirstType(), constraint.getSecondType(),
/*flags*/ None, constraint.getLocator());

case ConstraintKind::ValueMember:
case ConstraintKind::UnresolvedValueMember:
return simplifyMemberConstraint(constraint.getKind(),
Expand Down
4 changes: 2 additions & 2 deletions lib/Sema/CSSyntacticElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -624,8 +624,8 @@ class SyntacticElementConstraintGenerator
return;
}

// Convert the contextual type to the pattern, which establishes the
// bindings.
// Require a conversion from the contextual type to the pattern type, which
// establishes the bindings.
cs.addConstraint(ConstraintKind::Conversion, context.getType(), patternType,
locator);

Expand Down
7 changes: 7 additions & 0 deletions lib/Sema/Constraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Constraint::Constraint(ConstraintKind Kind, Type First, Type Second,
case ConstraintKind::EscapableFunctionOf:
case ConstraintKind::OpenedExistentialOf:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::OneWayEqual:
case ConstraintKind::OneWayBindParam:
case ConstraintKind::UnresolvedMemberChainBase:
Expand Down Expand Up @@ -152,6 +153,7 @@ Constraint::Constraint(ConstraintKind Kind, Type First, Type Second, Type Third,
case ConstraintKind::EscapableFunctionOf:
case ConstraintKind::OpenedExistentialOf:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::ApplicableFunction:
case ConstraintKind::DynamicCallableApplicableFunction:
case ConstraintKind::ValueMember:
Expand Down Expand Up @@ -309,6 +311,7 @@ Constraint *Constraint::clone(ConstraintSystem &cs) const {
case ConstraintKind::SelfObjectOfProtocol:
case ConstraintKind::DynamicCallableApplicableFunction:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::Defaultable:
case ConstraintKind::OneWayEqual:
case ConstraintKind::OneWayBindParam:
Expand Down Expand Up @@ -493,6 +496,9 @@ void Constraint::print(llvm::raw_ostream &Out, SourceManager *sm,
break;
case ConstraintKind::OptionalObject:
Out << " optional with object type "; break;
case ConstraintKind::EqualOrOptional:
Out << " equal to or optional of ";
break;
case ConstraintKind::BindOverload: {
Out << " bound to ";
auto overload = getOverloadChoice();
Expand Down Expand Up @@ -727,6 +733,7 @@ gatherReferencedTypeVars(Constraint *constraint,
case ConstraintKind::EscapableFunctionOf:
case ConstraintKind::OpenedExistentialOf:
case ConstraintKind::OptionalObject:
case ConstraintKind::EqualOrOptional:
case ConstraintKind::Defaultable:
case ConstraintKind::SubclassOf:
case ConstraintKind::ConformsTo:
Expand Down
Loading