Swift Compiler Error: "Expression too complex" on a string concatenation
Asked Answered
J

5

145

I find this amusing more than anything. I've fixed it, but I'm wondering about the cause. Here is the error: DataManager.swift:51:90: Expression was too complex to be solved in reasonable time; consider breaking up the expression into distinct sub-expressions. Why is it complaining? It seems like one of the most simple expressions possible.

The compiler points to the columns + ");"; section

func tableName() -> String { return("users"); } 

func createTableStatement(schema: [String]) -> String {

    var schema = schema;

    schema.append("id string");
    schema.append("created integer");
    schema.append("updated integer");
    schema.append("model blob");

    var columns: String = ",".join(schema);

    var statement = "create table if not exists " + self.tableName() + "(" + columns + ");";

    return(statement);
}

the fix is:

var statement = "create table if not exists " + self.tableName();
statement += "(" + columns + ");";

this also works (via @efischency) but I don't like it as much because I think the ( get lost:

var statement = "create table if not exists \(self.tableName()) (\(columns))"

Joseph answered 17/4, 2015 at 19:15 Comment(8)
Did you see if this works: var statement = "create table if not exists \(self.tableName()) (\(columns))"?Yorgos
String interpolation, as recommended by @efischency, is generally a better option than manual concatenation with +.Sloan
Sure, but that's not the point. I don't care if it's the "suggested" way or not, I just want to know why the compiler chokes on it. I have a solution that works, it's not about fixing the error, it's about understanding the error.Joseph
From what I've heard, the Swift compiler is still very much a work in progress. The team might appreciate a bug report on this.Cletis
I had no issue compiling this with 6.3.1. I had similar ridiculous messages in the past. We need to wait until Swift leaves its alpha state.Alight
I’m guessing the only person that can answer “Why” is Chris Lattner.Nonperishable
Just curious about your tags, this doesn't really have anything to do with iOS or functional programming, does it?Cowherb
@RenniePet: Swift is, among other things, a functional language. The implicit typing system is based on implicit typing in functional languages / compilers. I think the purpose of tags it to be accurate but also to put the question in front of people who are likely to have an answer. I thought functional language programmers were more likely to know the answer. At the time I asked, people only wrote Swift for iOS apps. If you have better suggestions, let me know.Joseph
B
185

I am not an expert on compilers - I don't know if this answer will "change how you think in a meaningful way," but my understanding of the problem is this:

It has to do with type inference. Each time you use the + operator, Swift has to search through all of the possible overloads for + and infer which version of + you are using. I counted just under 30 overloads for the + operator. That's a lot of possibilities, and when you chain 4 or 5 + operations together and ask the compiler to infer all of the arguments, you are asking a lot more than it might appear at first glance.

That inference can get complicated - for example, if you add a UInt8 and an Int using +, the output will be an Int, but there's some work that goes into evaluating the rules for mixing types with operators.

And when you are using literals, like the String literals in your example, the compiler doing the work of converting the String literal to a String, and then doing the work of infering the argument and return types for the + operator, etc.

If an expression is sufficiently complex - i.e., it requires the compiler to make too many inferences about the arguments and the operators - it quits and tells you that it quit.

Having the compiler quit once an expression reaches a certain level of complexity is intentional. The alternative is to let the compiler try and do it, and see if it can, but that is risky - the compiler could go on trying forever, bog down, or just crash. So my understanding is that there is a static threshold for the complexity of an expression that the compiler will not go beyond.

My understanding is that the Swift team is working on compiler optimizations that will make these errors less common. You can learn a little bit about it on the Apple Developer forums by clicking on this link.

On the Dev forums, Chris Lattner has asked people to file these errors as radar reports, because they are actively working on fixing them.

That is how I understand it after reading a number of posts here and on the Dev forum about it, but my understanding of compilers is naive, and I am hoping that someone with a deeper knowledge of how they handle these tasks will expand on what I have written here.

Bilabial answered 28/4, 2015 at 22:50 Comment(11)
I figured something to that effect, but it was a helpful answer none the less. Thanks for answering. Did you count the number of + operators by hand or is there some slick way I'm not aware of?Joseph
I just took a peek at it on SwiftDoc.org and counted them by hand. This is the page I'm talking about: swiftdoc.org/operator/plsBilabial
This is a bug, regardless of whether they will call it that. Other languages' compilers have no problem with code similar to what was posted. Suggesting the end user should fix it is silly.Phore
I just came across this with the following: let x = CGFloat((2.0 * 3.0 * Double(3%2) - 2.0)) Works fine if you convert to Double, or Int, or remove the inner pair of parentheses.Disparity
Type inference? What's the point on having a strong-typed language like Swift (in which you cannot even concatenate String + Int without having to cast the Int) in this riddiculous situation? Once again, Swift tries to solve problems nobody had in the first place.Cowlick
I've been noticing that Swift is generally much slower to compile than Obj C and I'm wondering if this is why. If you do this a lot but not so heavily in any one line that the compiler complains (or gets exponentially slower) I can imagine you wouldn't have any idea why Swift is so slow to compile. Until I added some + concats to my project, I was just thinking it was surprisingly fast compared to other Swift projects...Boorman
@Phore Not a bug, just bad language design if you ask me! Swift goes too far just trying to be different.Genagenappe
@Cowlick Why cast if you can do "Foo (42)"? You are not able to concatenate them because the language is strongly typed, and if you really need to you can overload + with a string and an int.Banerjee
@Banerjee Java is also strongly typed, but it's intelligent enough to know that String + int has no useful meaning unless you concatenate the String with the String representation of the int. I'm sorry but unlike Java, Swift is a stupidly non-backwards compatible piece of cr*p. I only identified a couple useful features, like Extensions. But, once again, XCode makes it sooo much not worth it...Cowlick
@Cowlick Concatenation with an operator is just not the Swift way, you can complain your hammer can't hammer in your screws or just get a screwdriver instead. If you want to concatenate a string and a variable in Swift you do "myString \(myVar)". You're complaining about having to cast your int, but that's not necessary at all.Banerjee
@Banerjee That has its own problems I don't really understand in Swift when dealing with optionals. Try the same "myString \(myVar)" for different myVar variable types (e.g. mandatory, optional) and see for yourself.Cowlick
C
31

This is almost same as the accepted answer but with some added dialogue (I had with Rob Napier, his other answers and Matt, Oliver, David from Slack) and links.

See the comments in this discussion. The gist of it is:

+ is heavily overloaded (Apple seems to have fixed this for some cases)

The + operator is heavily overloaded, as of now it has 27 different functions so if you are concatenating 4 strings ie you have 3 + operators the compiler has to check between 27 operators each time, so that's 27^3 times. But that's not it.

There is also a check to see if the lhs and rhs of the + functions are both valid if they are it calls through to core the append called. There you can see there are a number of somewhat intensive checks that can occur. If the string is stored non-contiguously, which appears to be the case if the string you’re dealing with is actually bridged to NSString. Swift then has to re-assemble all the byte array buffers into a single contiguous buffer and which requires creating new buffers along the way. and then you eventually get one buffer that contains the string you’re attempting to concatenate together.

In a nutshell there is 3 clusters of compiler checks that will slow you down ie each sub-expression has to be reconsidered in light of everything it might return. As a result concatenating strings with interpolation ie using " My fullName is \(firstName) \(LastName)" is much better than "My firstName is" + firstName + LastName since interpolation doesn't have any overloading

Swift 3 has made some improvements. For more information read How to merge multiple Arrays without slowing the compiler down?. Nonetheless the + operator is still overloaded and it's better to use string interpolation for longer strings


Usage of optionals (ongoing problem - solution available)

In this very simple project:

import UIKit

class ViewController: UIViewController {

    let p = Person()
    let p2 = Person2()

    func concatenatedOptionals() -> String {
        return (p2.firstName ?? "") + "" + (p2.lastName ?? "") + (p2.status ?? "")
    }

    func interpolationOptionals() -> String {
        return "\(p2.firstName ?? "") \(p2.lastName ?? "")\(p2.status ?? "")"
    }

    func concatenatedNonOptionals() -> String {
        return (p.firstName) + "" + (p.lastName) + (p.status)
    }

    func interpolatedNonOptionals() -> String {
        return "\(p.firstName) \(p.lastName)\(p.status)"
    }
}


struct Person {
    var firstName = "Swift"
    var lastName = "Honey"
    var status = "Married"
}

struct Person2 {
    var firstName: String? = "Swift"
    var lastName: String? = "Honey"
    var status: String? = "Married"
}

The compile time for the functions are as such:

21664.28ms  /Users/Honey/Documents/Learning/Foundational/CompileTime/CompileTime/ViewController.swift:16:10 instance method concatenatedOptionals()
2.31ms  /Users/Honey/Documents/Learning/Foundational/CompileTime/CompileTime/ViewController.swift:20:10 instance method interpolationOptionals()
0.96ms  /Users/Honey/Documents/Learning/Foundational/CompileTime/CompileTime/ViewController.swift:24:10 instance method concatenatedNonOptionals()
0.82ms  /Users/Honey/Documents/Learning/Foundational/CompileTime/CompileTime/ViewController.swift:28:10 instance method interpolatedNonOptionals()

Notice how crazy high the compilation duration for concatenatedOptionals is.

This can be solved by doing:

let emptyString: String = ""
func concatenatedOptionals() -> String {
    return (p2.firstName ?? emptyString) + emptyString + (p2.lastName ?? emptyString) + (p2.status ?? emptyString)
}

which compiles in 88ms

The root cause of the problem is that the compiler doesn't identify the "" as a String. It's actually ExpressibleByStringLiteral

The compiler will see ?? and will have to loop through all types that have conformed to this protocol, till it finds a type that can be a default to String. By Using emptyString which is hardcoded to String, the compiler no longer needs to loop through all conforming types of ExpressibleByStringLiteral

To learn how to log compilation times see here or here


Other similar answers by Rob Napier on SO:

Why string addition takes so long to build?

How to merge multiple Arrays without slowing the compiler down?

Swift Array contains function makes build times long

Celestial answered 15/9, 2016 at 15:12 Comment(0)
B
19

This is quite ridiculous no matter what you say! :)

enter image description here

But this gets passed easily

return "\(year) \(month) \(dayString) \(hour) \(min) \(weekDay)"
Bulter answered 21/2, 2018 at 21:57 Comment(0)
G
2

I had similar issue:

expression was too complex to be solved in reasonable time; consider breaking up the expression into distinct sub-expressions

In Xcode 9.3 line goes like this:

let media = entities.filter { (entity) -> Bool in

After changing it into something like this:

let media = entities.filter { (entity: Entity) -> Bool in

everything worked out.

Probably it has something to do with Swift compiler trying to infer data type from code around.

Guerdon answered 2/2, 2018 at 14:40 Comment(0)
F
0

Great news - this seems to be fixed in the upcoming Xcode 13.

I was filing radar reports for this:

http://openradar.appspot.com/radar?id=4962454186491904 https://bugreport.apple.com/web/?problemID=39206436

... and Apple has just confirmed that this is fixed.

I have tested all cases that I have with complex expressions and SwiftUI code and everything seems to work great in Xcode 13.

Hi Alex, Thanks for your patience, and thanks for your feedback. We believe this issue is resolved. Please test with the latest Xcode 13 beta 2 release and update your feedback report with your results by logging into https://feedbackassistant.apple.com or by using the Feedback Assistant app.

Fosque answered 30/6, 2021 at 10:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.