Merge pull request #85901 from slavapestov/too-complex-source-loc-6.3

Sema: Fix source location bookkeeping for 'reasonable time' diagnostic [6.3]
This commit is contained in:
Slava Pestov
2025-12-09 07:26:39 -05:00
committed by GitHub
10 changed files with 189 additions and 134 deletions

View File

@@ -236,36 +236,23 @@ public:
class ExpressionTimer {
public:
using AnchorType = llvm::PointerUnion<Expr *, ConstraintLocator *>;
private:
AnchorType Anchor;
ASTContext &Context;
ConstraintSystem &CS;
llvm::TimeRecord StartTime;
/// The number of seconds from creation until
/// this timer is considered expired.
unsigned ThresholdInSecs;
bool PrintDebugTiming;
bool PrintWarning;
public:
const static unsigned NoLimit = (unsigned) -1;
ExpressionTimer(AnchorType Anchor, ConstraintSystem &CS,
unsigned thresholdInSecs);
ExpressionTimer(ConstraintSystem &CS, unsigned thresholdInSecs);
~ExpressionTimer();
AnchorType getAnchor() const { return Anchor; }
SourceRange getAffectedRange() const;
unsigned getWarnLimit() const {
return Context.TypeCheckerOpts.WarnLongExpressionTypeChecking;
}
unsigned getWarnLimit() const;
llvm::TimeRecord startedAt() const { return StartTime; }
/// Return the elapsed process time (including fractional seconds)
@@ -2159,7 +2146,9 @@ struct ClosureIsolatedByPreconcurrency {
/// solution of which assigns concrete types to each of the type variables.
/// Constraint systems are typically generated given an (untyped) expression.
class ConstraintSystem {
private:
ASTContext &Context;
SourceRange CurrentRange;
public:
DeclContext *DC;
@@ -5390,6 +5379,9 @@ private:
/// \returns The selected conjunction.
Constraint *selectConjunction();
void diagnoseTooComplex(SourceLoc fallbackLoc,
SolutionResult &result);
/// Solve the system of constraints generated from provided expression.
///
/// \param target The target to generate constraints from.
@@ -5487,6 +5479,8 @@ private:
compareSolutions(ConstraintSystem &cs, ArrayRef<Solution> solutions,
const SolutionDiff &diff, unsigned idx1, unsigned idx2);
void startExpressionTimer();
public:
/// Increase the score of the given kind for the current (partial) solution
/// along the current solver path.
@@ -5524,7 +5518,6 @@ public:
std::optional<unsigned> findBestSolution(SmallVectorImpl<Solution> &solutions,
bool minimize);
public:
/// Apply a given solution to the target, producing a fully
/// type-checked target or \c None if an error occurred.
///
@@ -5577,7 +5570,14 @@ public:
/// resolved before any others.
void optimizeConstraints(Expr *e);
void startExpressionTimer(ExpressionTimer::AnchorType anchor);
/// Set the current sub-expression (of a multi-statement closure, etc) for
/// the purposes of diagnosing "reasonable time" errors.
void startExpression(ASTNode node);
/// The source range of the target being type checked.
SourceRange getCurrentSourceRange() const {
return CurrentRange;
}
/// Determine if we've already explored too many paths in an
/// attempt to solve this expression.
@@ -5590,53 +5590,7 @@ public:
return range.isValid() ? range : std::optional<SourceRange>();
}
bool isTooComplex(size_t solutionMemory) {
if (isAlreadyTooComplex.first)
return true;
auto CancellationFlag = getASTContext().CancellationFlag;
if (CancellationFlag && CancellationFlag->load(std::memory_order_relaxed))
return true;
auto markTooComplex = [&](SourceRange range, StringRef reason) {
if (isDebugMode()) {
if (solverState)
llvm::errs().indent(solverState->getCurrentIndent());
llvm::errs() << "(too complex: " << reason << ")\n";
}
isAlreadyTooComplex = {true, range};
return true;
};
auto used = getASTContext().getSolverMemory() + solutionMemory;
MaxMemory = std::max(used, MaxMemory);
auto threshold = getASTContext().TypeCheckerOpts.SolverMemoryThreshold;
if (MaxMemory > threshold) {
// No particular location for OoM problems.
return markTooComplex(SourceRange(), "exceeded memory limit");
}
if (Timer && Timer->isExpired()) {
// Disable warnings about expressions that go over the warning
// threshold since we're arbitrarily ending evaluation and
// emitting an error.
Timer->disableWarning();
return markTooComplex(Timer->getAffectedRange(), "exceeded time limit");
}
auto &opts = getASTContext().TypeCheckerOpts;
// Bail out once we've looked at a really large number of choices.
if (opts.SolverScopeThreshold && NumSolverScopes > opts.SolverScopeThreshold)
return markTooComplex(SourceRange(), "exceeded scope limit");
// Bail out once we've taken a really large number of steps.
if (opts.SolverTrailThreshold && NumTrailSteps > opts.SolverTrailThreshold)
return markTooComplex(SourceRange(), "exceeded trail limit");
return false;
}
bool isTooComplex(size_t solutionMemory);
bool isTooComplex(ArrayRef<Solution> solutions) {
if (isAlreadyTooComplex.first)

View File

@@ -76,8 +76,10 @@ public:
SolutionResult(const SolutionResult &other) = delete;
SolutionResult(SolutionResult &&other)
: kind(other.kind), numSolutions(other.numSolutions),
solutions(other.solutions) {
: kind(other.kind),
numSolutions(other.numSolutions),
solutions(other.solutions),
TooComplexAt(other.TooComplexAt) {
emittedDiagnostic = false;
other.kind = Error;
other.numSolutions = 0;

View File

@@ -1053,9 +1053,7 @@ TypeChecker::applyResultBuilderBodyTransform(FuncDecl *func, Type builderType) {
case SolutionResult::Kind::TooComplex:
reportSolutionsToSolutionCallback(salvagedResult);
func->diagnose(diag::expression_too_complex)
.highlight(func->getBodySourceRange());
salvagedResult.markAsDiagnosed();
cs.diagnoseTooComplex(func->getLoc(), salvagedResult);
return nullptr;
}

View File

@@ -829,9 +829,7 @@ bool ConstraintSystem::Candidate::solve(
// Allocate new constraint system for sub-expression.
ConstraintSystem cs(DC, std::nullopt);
// Set up expression type checker timer for the candidate.
cs.startExpressionTimer(E);
cs.startExpression(E);
// Generate constraints for the new system.
if (auto generatedExpr = cs.generateConstraints(E, DC)) {
@@ -1455,18 +1453,7 @@ ConstraintSystem::solve(SyntacticElementTarget &target,
return std::nullopt;
case SolutionResult::TooComplex: {
auto affectedRange = solution.getTooComplexAt();
// If affected range is unknown, let's use whole
// target.
if (!affectedRange)
affectedRange = target.getSourceRange();
getASTContext()
.Diags.diagnose(affectedRange->Start, diag::expression_too_complex)
.highlight(*affectedRange);
solution.markAsDiagnosed();
diagnoseTooComplex(target.getLoc(), solution);
return std::nullopt;
}
@@ -1501,6 +1488,19 @@ ConstraintSystem::solve(SyntacticElementTarget &target,
llvm_unreachable("Loop always returns");
}
void ConstraintSystem::diagnoseTooComplex(SourceLoc fallbackLoc,
SolutionResult &result) {
auto affectedRange = result.getTooComplexAt();
SourceLoc loc = (affectedRange ? affectedRange->Start : fallbackLoc);
auto diag = getASTContext().Diags.diagnose(loc, diag::expression_too_complex);
if (affectedRange)
diag.highlight(*affectedRange);
result.markAsDiagnosed();
}
SolutionResult
ConstraintSystem::solveImpl(SyntacticElementTarget &target,
FreeTypeVariableBinding allowFreeTypeVariables) {
@@ -1518,9 +1518,8 @@ ConstraintSystem::solveImpl(SyntacticElementTarget &target,
assert(!solverState && "cannot be used directly");
// Set up the expression type checker timer.
if (Expr *expr = target.getAsExpr())
startExpressionTimer(expr);
startExpression(expr);
if (generateConstraints(target, allowFreeTypeVariables))
return SolutionResult::forError();
@@ -1701,8 +1700,7 @@ bool ConstraintSystem::solveForCodeCompletion(
// Tell the constraint system what the contextual type is.
setContextualInfo(expr, target.getExprContextualTypeInfo());
// Set up the expression type checker timer.
startExpressionTimer(expr);
startExpression(expr);
shrink(expr);
}

View File

@@ -823,9 +823,13 @@ bool ConjunctionStep::attempt(const ConjunctionElement &element) {
// (expression) gets a fresh time slice to get solved. This
// is important for closures with large number of statements
// in them.
if (CS.Timer) {
if (CS.Timer)
CS.Timer.reset();
CS.startExpressionTimer(element.getLocator());
{
auto *locator = element.getLocator();
auto anchor = simplifyLocatorToAnchor(locator);
CS.startExpression(anchor ? anchor : locator->getAnchor());
}
auto success = element.attempt(CS);

View File

@@ -867,8 +867,7 @@ class ConjunctionStep : public BindingStep<ConjunctionElementProducer> {
/// The number of milliseconds until outer constraint system
/// is considered "too complex" if timer is enabled.
std::optional<std::pair<ExpressionTimer::AnchorType, unsigned>>
OuterTimeRemaining = std::nullopt;
std::optional<unsigned> OuterTimeRemaining = std::nullopt;
/// Conjunction constraint associated with this step.
Constraint *Conjunction;
@@ -910,7 +909,7 @@ public:
if (cs.Timer) {
auto remainingTime = cs.Timer->getRemainingProcessTimeInSeconds();
OuterTimeRemaining.emplace(cs.Timer->getAnchor(), remainingTime);
OuterTimeRemaining.emplace(remainingTime);
}
}
@@ -925,11 +924,8 @@ public:
if (HadFailure)
restoreBestScore();
if (OuterTimeRemaining) {
auto anchor = OuterTimeRemaining->first;
auto remainingTime = OuterTimeRemaining->second;
CS.Timer.emplace(anchor, CS, remainingTime);
}
if (OuterTimeRemaining)
CS.Timer.emplace(CS, *OuterTimeRemaining);
}
StepResult resume(bool prevFailed) override;

View File

@@ -54,44 +54,82 @@ using namespace inference;
#define DEBUG_TYPE "ConstraintSystem"
ExpressionTimer::ExpressionTimer(AnchorType Anchor, ConstraintSystem &CS,
unsigned thresholdInSecs)
: Anchor(Anchor), Context(CS.getASTContext()),
StartTime(llvm::TimeRecord::getCurrentTime()),
ThresholdInSecs(thresholdInSecs),
PrintDebugTiming(CS.getASTContext().TypeCheckerOpts.DebugTimeExpressions),
PrintWarning(true) {}
void ConstraintSystem::startExpression(ASTNode node) {
CurrentRange = node.getSourceRange();
SourceRange ExpressionTimer::getAffectedRange() const {
ASTNode anchor;
startExpressionTimer();
}
if (auto *locator = Anchor.dyn_cast<ConstraintLocator *>()) {
anchor = simplifyLocatorToAnchor(locator);
// If locator couldn't be simplified down to a single AST
// element, let's use its root.
if (!anchor)
anchor = locator->getAnchor();
} else {
anchor = cast<Expr *>(Anchor);
bool ConstraintSystem::isTooComplex(size_t solutionMemory) {
if (isAlreadyTooComplex.first)
return true;
auto CancellationFlag = getASTContext().CancellationFlag;
if (CancellationFlag && CancellationFlag->load(std::memory_order_relaxed))
return true;
auto markTooComplex = [&](SourceRange range, StringRef reason) {
if (isDebugMode()) {
if (solverState)
llvm::errs().indent(solverState->getCurrentIndent());
llvm::errs() << "(too complex: " << reason << ")\n";
}
isAlreadyTooComplex = {true, range};
return true;
};
auto used = getASTContext().getSolverMemory() + solutionMemory;
MaxMemory = std::max(used, MaxMemory);
auto threshold = getASTContext().TypeCheckerOpts.SolverMemoryThreshold;
if (MaxMemory > threshold) {
// Bail once we've used too much constraint solver arena memory.
return markTooComplex(getCurrentSourceRange(), "exceeded memory limit");
}
return anchor.getSourceRange();
if (Timer && Timer->isExpired()) {
// Disable warnings about expressions that go over the warning
// threshold since we're arbitrarily ending evaluation and
// emitting an error.
Timer->disableWarning();
return markTooComplex(getCurrentSourceRange(), "exceeded time limit");
}
auto &opts = getASTContext().TypeCheckerOpts;
// Bail out once we've looked at a really large number of choices.
if (opts.SolverScopeThreshold && NumSolverScopes > opts.SolverScopeThreshold)
return markTooComplex(getCurrentSourceRange(), "exceeded scope limit");
// Bail out once we've taken a really large number of steps.
if (opts.SolverTrailThreshold && NumTrailSteps > opts.SolverTrailThreshold)
return markTooComplex(getCurrentSourceRange(), "exceeded trail limit");
return false;
}
ExpressionTimer::ExpressionTimer(ConstraintSystem &CS, unsigned thresholdInSecs)
: CS(CS),
StartTime(llvm::TimeRecord::getCurrentTime()),
ThresholdInSecs(thresholdInSecs),
PrintWarning(true) {}
unsigned ExpressionTimer::getWarnLimit() const {
return CS.getASTContext().TypeCheckerOpts.WarnLongExpressionTypeChecking;
}
ExpressionTimer::~ExpressionTimer() {
auto elapsed = getElapsedProcessTimeInFractionalSeconds();
unsigned elapsedMS = static_cast<unsigned>(elapsed * 1000);
auto &ctx = CS.getASTContext();
if (PrintDebugTiming) {
auto range = CS.getCurrentSourceRange();
if (ctx.TypeCheckerOpts.DebugTimeExpressions) {
// Round up to the nearest 100th of a millisecond.
llvm::errs() << llvm::format("%0.2f", std::ceil(elapsed * 100000) / 100)
<< "ms\t";
if (auto *E = Anchor.dyn_cast<Expr *>()) {
E->getLoc().print(llvm::errs(), Context.SourceMgr);
} else {
auto *locator = cast<ConstraintLocator *>(Anchor);
locator->dump(&Context.SourceMgr, llvm::errs());
}
range.Start.print(llvm::errs(), ctx.SourceMgr);
llvm::errs() << "\n";
}
@@ -103,13 +141,11 @@ ExpressionTimer::~ExpressionTimer() {
if (WarnLimit == 0 || elapsedMS < WarnLimit)
return;
auto sourceRange = getAffectedRange();
if (sourceRange.Start.isValid()) {
Context.Diags
.diagnose(sourceRange.Start, diag::debug_long_expression, elapsedMS,
if (range.Start.isValid()) {
ctx.Diags
.diagnose(range.Start, diag::debug_long_expression, elapsedMS,
WarnLimit)
.highlight(sourceRange);
.highlight(range);
}
}
@@ -140,7 +176,7 @@ ConstraintSystem::~ConstraintSystem() {
}
}
void ConstraintSystem::startExpressionTimer(ExpressionTimer::AnchorType anchor) {
void ConstraintSystem::startExpressionTimer() {
ASSERT(!Timer);
const auto &opts = getASTContext().TypeCheckerOpts;
@@ -156,7 +192,7 @@ void ConstraintSystem::startExpressionTimer(ExpressionTimer::AnchorType anchor)
timeout = ExpressionTimer::NoLimit;
}
Timer.emplace(anchor, *this, timeout);
Timer.emplace(*this, timeout);
}
void ConstraintSystem::incrementScopeCounter() {
@@ -3731,6 +3767,10 @@ void constraints::simplifyLocator(ASTNode &anchor,
case ConstraintLocator::Condition: {
if (auto *condStmt = getAsStmt<LabeledConditionalStmt>(anchor)) {
anchor = &condStmt->getCond().front();
} else if (auto *whileStmt = getAsStmt<RepeatWhileStmt>(anchor)) {
anchor = whileStmt->getCond();
} else if (auto *assertStmt = getAsStmt<PoundAssertStmt>(anchor)) {
anchor = assertStmt->getCondition();
} else {
anchor = castToExpr<TernaryExpr>(anchor)->getCondExpr();
}

View File

@@ -0,0 +1,19 @@
// RUN: %target-typecheck-verify-swift -solver-scope-threshold=10
// Note: the scope threshold is intentionally set low so that the expression will fail.
//
// The purpose of the test is to ensure the diagnostic points at the second statement in
// the closure, and not the closure itself.
//
// If the expression becomes very fast and we manage to type check it with fewer than
// 10 scopes, please *do not* remove the expected error! Instead, make the expression
// more complex again.
let s = ""
let n = 0
let closure = {
let _ = 0
let _ = "" + s + "" + s + "" + s + "" + n + "" // expected-error {{reasonable time}}
let _ = 0
}

View File

@@ -0,0 +1,44 @@
// RUN: %target-typecheck-verify-swift -target %target-cpu-apple-macosx10.15 -swift-version 5
// REQUIRES: objc_interop
// REQUIRES: OS=macosx
// https://forums.swift.org/t/roadmap-for-improving-the-type-checker/82952/9
//
// The purpose of the test is to ensure the diagnostic points at the right statement in
// the function body, and not the function declaration itself.
//
// Ideally, we would produce a useful diagnostic here. Once we are able to do that, we
// will need to devise a new test which complains with 'reasonable time' to ensure the
// source location remains correct.
import SwiftUI
struct ContentView: View {
@State var selection = ""
@State var a: Int?
@State var b: Int?
@State var c: Int?
var body: some View {
ScrollView {
VStack {
Picker(selection: $selection) {
ForEach(["a", "b", "c"], id: \.self) {
Text($0) // expected-error {{reasonable time}}
.foregroundStyl(.red) // Typo is here
}
} label: {
}
.pickerStyle(.segmented)
}
.padding(.horizontal)
}
.onChange(of: a) { oldValue, newValue in
}
.onChange(of: b) { oldValue, newValue in
}
.onChange(of: c) { oldValue, newValue in
}
}
}

View File

@@ -11,8 +11,8 @@ struct rdar19612086 {
let x = 1.0
var description : String {
return "\(i)" + Stringly(format: "%.2f", x) + // expected-error {{reasonable time}}
"\(i+1)" + Stringly(format: "%.2f", x) +
return "\(i)" + Stringly(format: "%.2f", x) +
"\(i+1)" + Stringly(format: "%.2f", x) + // expected-error {{reasonable time}}
"\(i+2)" + Stringly(format: "%.2f", x) +
"\(i+3)" + Stringly(format: "%.2f", x) +
"\(i+4)" + Stringly(format: "%.2f", x) +