Skip to content

[ConstraintSystem] Implement disjunction favoring algorithm behind a flag #82574

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
merged 52 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f650c0a
[CSBindings] Prevent `BindingSet::isViable` from dropping viable bind…
xedin Nov 23, 2024
69bd48e
[CSSimplify] CGFloat-Double: Rank narrowing correctly when result is …
xedin Nov 26, 2024
087c36c
[ConstraintSystem] Fix `getEffectiveOverloadType` handling of `mutati…
xedin Dec 17, 2024
c18c626
[ConstraintSystem] Fix `getEffectiveOverloadType` to recognize when b…
xedin Jun 16, 2025
1b2ba7e
[CSStep] Modify `selectDisjunction` to return favored choices directly
xedin Feb 10, 2023
0871358
[TypeChecker] Update `async` initializer warning downgrade to check t…
xedin May 17, 2025
c04068d
[ConstraintSystem] Initial implementation of disjunction optimizer
xedin Jun 25, 2025
2957da3
[ConstraintSystem] Disable performance hacks by default
xedin Jun 25, 2025
0a5bd78
[ConstraintSystem] Narrowly disable `tryOptimizeGenericDisjunction` w…
xedin Sep 22, 2024
80e12b9
[CSOptimizer] Improve contextual result type handling during overload…
xedin Feb 6, 2025
2f8a343
[CSOptimizer] Infer result types through ternary expressions
xedin Feb 6, 2025
6c28cdf
[CSOptimizer] Infer argument candidates through optionals
xedin Feb 6, 2025
899b2bc
[CSOptimizer] Allow matching against CGFloat as a contextual result type
xedin Feb 6, 2025
0dfb204
[CSOptimizer] Infer types from init calls used as arguments
xedin Feb 6, 2025
84d034c
[CSOptimizer] Don't reduce the ranking for non-default literal types
xedin Feb 6, 2025
666aa24
[CSOptimizer] Add support for `??` operator
xedin Feb 7, 2025
26b86c2
[Tests] NFC: Add a test-case with literal chain passed as an argument…
xedin Feb 8, 2025
b96139e
[CSOptimizer] Emulate old hack for unary argument matching more preci…
xedin Feb 8, 2025
0e0b5f9
[CSOptimizer] Always prefer a disjunction with a single active choice
xedin Feb 9, 2025
0e72686
[CSSolver] Attempt choices marked as disfavored at the end of partition
xedin Feb 9, 2025
2646efa
[CSOptimizer] Expand literal support to bools, strings and dictionaries
xedin Feb 11, 2025
b936900
[CSOptimizer] Support non-operator generic choices with simple signat…
xedin Feb 11, 2025
aedc4fe
[CSOptimizer] Remove `selectBestBindingDisjunction` hack
xedin Feb 16, 2025
ac24a8e
[CSOptimizer] Introduce `_OptionalNilComparisonType` as candidate for…
xedin Feb 16, 2025
0525818
[CSOptimizer] Allow matching candidates with optional types against g…
xedin Feb 17, 2025
4eec3f6
[CSOptimizer] Introduce a drop of inference to preserved unary argume…
xedin Feb 17, 2025
df4ae0a
[CSOptimizer] Update candidate selection to use arithmetic operator c…
xedin Feb 18, 2025
bcc749f
[CSOptimizer] Reset the overall score of operator disjunctions that i…
xedin Feb 18, 2025
e2fe558
[CSOptimizer] Introduce a way to preference disjunctions before score…
xedin Feb 18, 2025
617338f
[CSOptimizer] Prevent candidate inference from unresolved generic par…
xedin Feb 20, 2025
a953dfe
[Tests] NFC: Adjust scope threshold for rdar://rdar31742586 test-case
xedin Feb 21, 2025
073b48c
[CSOptimizer] Restrict unary argument legacy favoring behavior to `Ap…
xedin Feb 28, 2025
1dab584
[CSOptimizer] Account for speculative scores only when matching opera…
xedin Mar 2, 2025
e280569
[CSOptimizer] Fix scoring while matching against partially resolved p…
xedin Mar 2, 2025
5aa3859
[CSOptimizer] Disable unary argument hack if overload set has require…
xedin Mar 3, 2025
125abed
[CSOptimizer] Detect when candidate comes from string interpolation
xedin Mar 4, 2025
e7b351d
[CSOptimizer] NFC: Use builder pattern to construct `DisjunctionInfo`
xedin Mar 4, 2025
9e97b8e
[CSOptimizer] Skip disfavored choices when they would result in a wor…
xedin Mar 10, 2025
db0a9de
[CSOptimizer] Account for the fact that sometimes all initializer cho…
xedin Mar 11, 2025
3616dab
[Tests] NFC: Move Double/CGFloat test because it tests local type met…
xedin Mar 11, 2025
b90fc2b
[CSOptimizer] Don't attempt to walk into literals while analyzing ope…
xedin Mar 13, 2025
ea47e3c
[CSOptimizer] NFC: Adopt new `Constraint` and `ConstraintGraph` APIs
xedin Mar 15, 2025
eb78e27
[CSOptimizer] Consider choices marked as `@_disfavoredOverload`
xedin Mar 15, 2025
8037bbc
[CSOptimizer] NFC: Adopt to changes in `getParameterList()` API
xedin Apr 21, 2025
479e61b
[CSOptimizer] Adopt to removal of `TypeBase::isArrayType()` by switch…
xedin May 17, 2025
2520d40
[CSOptimizer] Allow literal inference inside of operator chains
xedin Jun 13, 2025
ef2be0d
[CSOptimizer] Rank operators with the same score based on speculative…
xedin Jun 13, 2025
979e046
[CSOptimizer] Rework `inferTypeOfArithmeticOperatorChain`
xedin Jun 16, 2025
eee40b4
[CSGen] Make collection subscript result type inference more principled
xedin Jun 23, 2025
3efb948
[ConstraintSystem] Add a option to re-enable performance hacks
xedin Jun 27, 2025
c1e0e66
[TypeChecker] Implement a per-module block list for disjunction optim…
xedin Jun 27, 2025
4591884
[Tests] NFC: Remove `-disable-constraint-solver-performance-hacks` si…
xedin Jun 28, 2025
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
1 change: 1 addition & 0 deletions include/swift/AST/KnownStdlibTypes.def
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ KNOWN_STDLIB_TYPE_DECL(WritableKeyPath, NominalTypeDecl, 2)
KNOWN_STDLIB_TYPE_DECL(ReferenceWritableKeyPath, NominalTypeDecl, 2)

KNOWN_STDLIB_TYPE_DECL(Optional, EnumDecl, 1)
KNOWN_STDLIB_TYPE_DECL(_OptionalNilComparisonType, NominalTypeDecl, 0)

KNOWN_STDLIB_TYPE_DECL(OptionSet, NominalTypeDecl, 1)

Expand Down
16 changes: 16 additions & 0 deletions include/swift/AST/TypeCheckRequests.h
Original file line number Diff line number Diff line change
Expand Up @@ -5370,6 +5370,22 @@ class DefaultIsolationInSourceFileRequest
bool isCached() const { return true; }
};

class ModuleHasTypeCheckerPerformanceHacksEnabledRequest
: public SimpleRequest<ModuleHasTypeCheckerPerformanceHacksEnabledRequest,
bool(const ModuleDecl *),
RequestFlags::Cached> {
public:
using SimpleRequest::SimpleRequest;

private:
friend SimpleRequest;

bool evaluate(Evaluator &evaluator, const ModuleDecl *module) const;

public:
bool isCached() const { return true; }
};

#define SWIFT_TYPEID_ZONE TypeChecker
#define SWIFT_TYPEID_HEADER "swift/AST/TypeCheckerTypeIDZone.def"
#include "swift/Basic/DefineTypeIDZone.h"
Expand Down
4 changes: 4 additions & 0 deletions include/swift/AST/TypeCheckerTypeIDZone.def
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,7 @@ SWIFT_REQUEST(TypeChecker, SemanticAvailabilitySpecRequest,
SWIFT_REQUEST(TypeChecker, DefaultIsolationInSourceFileRequest,
std::optional<DefaultIsolation>(const SourceFile *),
Cached, NoLocationInfo)

SWIFT_REQUEST(TypeChecker, ModuleHasTypeCheckerPerformanceHacksEnabledRequest,
bool(const ModuleDecl *),
Cached, NoLocationInfo)
1 change: 1 addition & 0 deletions include/swift/Basic/BlockListAction.def
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ BLOCKLIST_ACTION(ShouldUseLayoutStringValueWitnesses)
BLOCKLIST_ACTION(ShouldDisableOwnershipVerification)
BLOCKLIST_ACTION(SkipEmittingFineModuleTrace)
BLOCKLIST_ACTION(SkipIndexingModule)
BLOCKLIST_ACTION(ShouldUseTypeCheckerPerfHacks)

#undef BLOCKLIST_ACTION
4 changes: 2 additions & 2 deletions include/swift/Basic/LangOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -986,8 +986,8 @@ namespace swift {
/// Enable experimental operator designated types feature.
bool EnableOperatorDesignatedTypes = false;

/// Disable constraint system performance hacks.
bool DisableConstraintSolverPerformanceHacks = false;
/// Enable old constraint system performance hacks.
bool EnableConstraintSolverPerformanceHacks = false;

/// See \ref FrontendOptions.PrintFullConvention
bool PrintFullConvention = false;
Expand Down
4 changes: 2 additions & 2 deletions include/swift/Option/FrontendOptions.td
Original file line number Diff line number Diff line change
Expand Up @@ -862,8 +862,8 @@ def solver_trail_threshold_EQ : Joined<["-"], "solver-trail-threshold=">,
def solver_disable_splitter : Flag<["-"], "solver-disable-splitter">,
HelpText<"Disable the component splitter phase of expression type checking">;

def disable_constraint_solver_performance_hacks : Flag<["-"], "disable-constraint-solver-performance-hacks">,
HelpText<"Disable all the hacks in the constraint solver">;
def enable_constraint_solver_performance_hacks : Flag<["-"], "enable-constraint-solver-performance-hacks">,
HelpText<"Enable all the old hacks in the constraint solver">;

def enable_operator_designated_types :
Flag<["-"], "enable-operator-designated-types">,
Expand Down
6 changes: 6 additions & 0 deletions include/swift/Sema/CSBindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,12 @@ class BindingSet {
void forEachLiteralRequirement(
llvm::function_ref<void(KnownProtocolKind)> callback) const;

void forEachAdjacentVariable(
llvm::function_ref<void(TypeVariableType *)> callback) const {
for (auto *typeVar : AdjacentVars)
callback(typeVar);
}

/// Return a literal requirement that has the most impact on the binding
/// score.
LiteralBindingKind getLiteralForScore() const;
Expand Down
49 changes: 40 additions & 9 deletions include/swift/Sema/ConstraintSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,18 @@ class TypeVariableType::Implementation {
/// literal (represented by `ArrayExpr` and `DictionaryExpr` in AST).
bool isCollectionLiteralType() const;

/// Determine whether this type variable represents a literal such
/// as an integer value, a floating-point value with and without a sign.
bool isNumberLiteralType() const;

/// Determine whether this type variable represents a result type of a
/// function call.
bool isFunctionResult() const;

/// Determine whether this type variable represents a type of the ternary
/// operator.
bool isTernary() const;

/// Retrieve the representative of the equivalence class to which this
/// type variable belongs.
///
Expand Down Expand Up @@ -1952,6 +1964,9 @@ enum class ConstraintSystemFlags {

/// Disable macro expansions.
DisableMacroExpansions = 0x80,

/// Enable old type-checker performance hacks.
EnablePerformanceHacks = 0x100,
};

/// Options that affect the constraint system as a whole.
Expand Down Expand Up @@ -3591,11 +3606,10 @@ class ConstraintSystem {
return Options.contains(ConstraintSystemFlags::ForCodeCompletion);
}

/// Check whether type-checker performance hacks has been explicitly
/// disabled by a flag.
/// Check whether old type-checker performance hacks has been explicitly
/// enabled.
bool performanceHacksEnabled() const {
return !getASTContext()
.TypeCheckerOpts.DisableConstraintSolverPerformanceHacks;
return Options.contains(ConstraintSystemFlags::EnablePerformanceHacks);
}

/// Log and record the application of the fix. Return true iff any
Expand Down Expand Up @@ -5397,8 +5411,12 @@ class ConstraintSystem {

/// Pick a disjunction from the InactiveConstraints list.
///
/// \returns The selected disjunction.
Constraint *selectDisjunction();
/// \returns The selected disjunction and a set of it's favored choices.
std::optional<std::pair<Constraint *, llvm::TinyPtrVector<Constraint *>>>
selectDisjunction();

/// The old method that is only used when performance hacks are enabled.
Constraint *selectDisjunctionWithHacks();

/// Pick a conjunction from the InactiveConstraints list.
///
Expand Down Expand Up @@ -6098,6 +6116,12 @@ class DisjunctionChoice {
return false;
}

bool isDisfavored() const {
if (auto *decl = getOverloadChoiceDecl(Choice))
return decl->getAttrs().hasAttribute<DisfavoredOverloadAttr>();
return false;
}

bool isBeginningOfPartition() const { return IsBeginningOfPartition; }

// FIXME: All three of the accessors below are required to support
Expand Down Expand Up @@ -6332,7 +6356,8 @@ class DisjunctionChoiceProducer : public BindingProducer<DisjunctionChoice> {
public:
using Element = DisjunctionChoice;

DisjunctionChoiceProducer(ConstraintSystem &cs, Constraint *disjunction)
DisjunctionChoiceProducer(ConstraintSystem &cs, Constraint *disjunction,
llvm::TinyPtrVector<Constraint *> &favorites)
: BindingProducer(cs, disjunction->shouldRememberChoice()
? disjunction->getLocator()
: nullptr),
Expand All @@ -6342,6 +6367,11 @@ class DisjunctionChoiceProducer : public BindingProducer<DisjunctionChoice> {
assert(disjunction->getKind() == ConstraintKind::Disjunction);
assert(!disjunction->shouldRememberChoice() || disjunction->getLocator());

// Mark constraints as favored. This information
// is going to be used by partitioner.
for (auto *choice : favorites)
cs.favorConstraint(choice);

// Order and partition the disjunction choices.
partitionDisjunction(Ordering, PartitionBeginning);
}
Expand Down Expand Up @@ -6386,8 +6416,9 @@ class DisjunctionChoiceProducer : public BindingProducer<DisjunctionChoice> {
// Partition the choices in the disjunction into groups that we will
// iterate over in an order appropriate to attempt to stop before we
// have to visit all of the options.
void partitionDisjunction(SmallVectorImpl<unsigned> &Ordering,
SmallVectorImpl<unsigned> &PartitionBeginning);
void
partitionDisjunction(SmallVectorImpl<unsigned> &Ordering,
SmallVectorImpl<unsigned> &PartitionBeginning);

/// Partition the choices in the range \c first to \c last into groups and
/// order the groups in the best order to attempt based on the argument
Expand Down
4 changes: 2 additions & 2 deletions lib/Frontend/CompilerInvocation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2009,8 +2009,8 @@ static bool ParseTypeCheckerArgs(TypeCheckerOptions &Opts, ArgList &Args,
SWIFT_ONONE_SUPPORT);
}

Opts.DisableConstraintSolverPerformanceHacks |=
Args.hasArg(OPT_disable_constraint_solver_performance_hacks);
Opts.EnableConstraintSolverPerformanceHacks |=
Args.hasArg(OPT_enable_constraint_solver_performance_hacks);

Opts.EnableOperatorDesignatedTypes |=
Args.hasArg(OPT_enable_operator_designated_types);
Expand Down
1 change: 1 addition & 0 deletions lib/Sema/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ add_swift_host_library(swiftSema STATIC
CSStep.cpp
CSTrail.cpp
CSFix.cpp
CSOptimizer.cpp
CSDiagnostics.cpp
CodeSynthesis.cpp
CodeSynthesisDistributedActor.cpp
Expand Down
33 changes: 31 additions & 2 deletions lib/Sema/CSBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@ using namespace swift;
using namespace constraints;
using namespace inference;


void ConstraintGraphNode::initBindingSet() {
ASSERT(!hasBindingSet());
ASSERT(forRepresentativeVar());

Set.emplace(CG.getConstraintSystem(), TypeVar, Potential);
}

/// Check whether there exists a type that could be implicitly converted
/// to a given type i.e. is the given type is Double or Optional<..> this
/// function is going to return true because CGFloat could be converted
/// to a Double and non-optional value could be injected into an optional.
static bool hasConversions(Type);

static std::optional<Type> checkTypeOfBinding(TypeVariableType *typeVar,
Type type);

Expand Down Expand Up @@ -1348,7 +1353,31 @@ bool BindingSet::isViable(PotentialBinding &binding, bool isTransitive) {
if (!existingNTD || NTD != existingNTD)
continue;

// FIXME: What is going on here needs to be thoroughly re-evaluated.
// What is going on in this method needs to be thoroughly re-evaluated!
//
// This logic aims to skip dropping bindings if
// collection type has conversions i.e. in situations like:
//
// [$T1] conv $T2
// $T2 conv [(Int, String)]
// $T2.Element equal $T5.Element
//
// `$T1` could be bound to `(i: Int, v: String)` after
// `$T2` is bound to `[(Int, String)]` which is is a problem
// because it means that `$T2` was attempted to early
// before the solver had a chance to discover all viable
// bindings.
//
// Let's say existing binding is `[(Int, String)]` and
// relation is "exact", in this case there is no point
// tracking `[$T1]` because upcasts are only allowed for
// subtype and other conversions.
if (existing->Kind != AllowedBindingKind::Exact) {
if (existingType->isKnownStdlibCollectionType() &&
hasConversions(existingType)) {
continue;
}
}

// If new type has a type variable it shouldn't
// be considered viable.
Expand Down
103 changes: 58 additions & 45 deletions lib/Sema/CSGen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,61 @@ namespace {
return tv;
}

/// Attempt to infer a result type of a subscript reference where
/// the base type is either a stdlib Array or a Dictionary type.
/// This is a more principled version of the old performance hack
/// that used "favored" types deduced by the constraint optimizer
/// and is important to maintain pre-existing solver behavior.
Type inferCollectionSubscriptResultType(Type baseTy,
ArgumentList *argumentList) {
auto isLValueBase = false;
auto baseObjTy = baseTy;
if (baseObjTy->is<LValueType>()) {
isLValueBase = true;
baseObjTy = baseObjTy->getWithoutSpecifierType();
}

auto subscriptResultType = [&isLValueBase](Type valueTy,
bool isOptional) -> Type {
Type outputTy = isOptional ? OptionalType::get(valueTy) : valueTy;
return isLValueBase ? LValueType::get(outputTy) : outputTy;
};

if (auto *argument = argumentList->getUnlabeledUnaryExpr()) {
auto argumentTy = CS.getType(argument);

auto elementTy = baseObjTy->getArrayElementType();

if (!elementTy)
elementTy = baseObjTy->getInlineArrayElementType();

if (elementTy) {
if (auto arraySliceTy =
dyn_cast<ArraySliceType>(baseObjTy.getPointer())) {
baseObjTy = arraySliceTy->getDesugaredType();
}

if (argumentTy->isInt() || isExpr<IntegerLiteralExpr>(argument))
return subscriptResultType(elementTy, /*isOptional*/ false);
} else if (auto dictTy = CS.isDictionaryType(baseObjTy)) {
auto [keyTy, valueTy] = *dictTy;

if (keyTy->isString() &&
(isExpr<StringLiteralExpr>(argument) ||
isExpr<InterpolatedStringLiteralExpr>(argument)))
return subscriptResultType(valueTy, /*isOptional*/ true);

if (keyTy->isInt() && isExpr<IntegerLiteralExpr>(argument))
return subscriptResultType(valueTy, /*isOptional*/ true);

if (keyTy->isEqual(argumentTy))
return subscriptResultType(valueTy, /*isOptional*/ true);
}
}

return Type();
}

/// Add constraints for a subscript operation.
Type addSubscriptConstraints(
Expr *anchor, Type baseTy, ValueDecl *declOrNull, ArgumentList *argList,
Expand All @@ -1074,52 +1129,10 @@ namespace {

Type outputTy;

// For an integer subscript expression on an array slice type, instead of
// introducing a new type variable we can easily obtain the element type.
if (isa<SubscriptExpr>(anchor)) {

auto isLValueBase = false;
auto baseObjTy = baseTy;
if (baseObjTy->is<LValueType>()) {
isLValueBase = true;
baseObjTy = baseObjTy->getWithoutSpecifierType();
}

auto elementTy = baseObjTy->getArrayElementType();
// Attempt to infer the result type of a stdlib collection subscript.
if (isa<SubscriptExpr>(anchor))
outputTy = inferCollectionSubscriptResultType(baseTy, argList);

if (!elementTy)
elementTy = baseObjTy->getInlineArrayElementType();

if (elementTy) {

if (auto arraySliceTy =
dyn_cast<ArraySliceType>(baseObjTy.getPointer())) {
baseObjTy = arraySliceTy->getDesugaredType();
}

if (argList->isUnlabeledUnary() &&
isa<IntegerLiteralExpr>(argList->getExpr(0))) {

outputTy = elementTy;

if (isLValueBase)
outputTy = LValueType::get(outputTy);
}
} else if (auto dictTy = CS.isDictionaryType(baseObjTy)) {
auto keyTy = dictTy->first;
auto valueTy = dictTy->second;

if (argList->isUnlabeledUnary()) {
auto argTy = CS.getType(argList->getExpr(0));
if (isFavoredParamAndArg(CS, keyTy, argTy)) {
outputTy = OptionalType::get(valueTy);
if (isLValueBase)
outputTy = LValueType::get(outputTy);
}
}
}
}

if (outputTy.isNull()) {
outputTy = CS.createTypeVariable(resultLocator,
TVO_CanBindToLValue | TVO_CanBindToNoEscape);
Expand Down
Loading