Replacing the Type Checker

A Swing-For-The-Fences Question:

Has there ever been any serious discussion about replacing the entire approach to type-checking in Swift? That is, moving away from inference and the constraint-solver to a more explicit declaration of types?

Reason:

This sums it up pretty well: Why The Swift Compiler is Slow

He gives some great examples. They closely match my experience working with Swift in a large SwiftUI codebase. Compiling is slow, but the real killer is that the compiler is often totally unable to help find issues. I end up commenting-out swaths of code until the can't type check in reasonable time message goes away, then slowly un-commenting things until I find the problem.

That problem has often been dead simple, such as typing .someFunction(withID: "blah") instead of .someFunction(forID: "blah") (It does not help that Xcode's AI autocomplete now suggests totally fictitious method names, but that is a separate issue.) The compiler can't even show me this simple problem anymore.

I also explicitly declare types virtually everywhere that I can; I don't rely on inference. But that's not always possible in SwiftUI and because you can't use procedural tools like for loops in many places, you have to fall back on more complex functional-style expressions. (filter, map, forEach, etc.)

The Bottom Line

I distinctly recall Chris's brevity is an explicit non-goal statement for the language. Perhaps Swift 7 could consider applying that to the type-checker? Requiring developers to more explicitly declare types so that the solver goes away and we get excellent compiler performance and diagnostics as a result?

Or maybe you guys already have a vision for how to solve this? It feels like Swift's type-checker has backed the language into a corner with SwiftUI and each year the problem is getting worse.

NB: I understand the problem is difficult. I'm not attacking anyone about it. Lately it just frequently feels like I'm writing and debugging PHP in a compiled language.

7 Likes

So do you propose to break 99.9% codebases written in Swift?
Decomposition into functions and types is what will solve your problem. If you have to comment 'large swaths' of code out to find a problem, that means that you have large swaths of code written in a single function. That is not an optimal approach and a lot of codebases have restrictions on a function body length.
Programming languages generally require more and more time to compile as the hardware capabilities grow. In C++, that was initially optimized for very fast compilation, you now have auto with even wider capabilities than in Swift (it can be applied in function declarations). The type names are getting bigger and more complex, and writing them down explicitly is a pain, SwiftUI is a good example here, but lots of strongly-typed languages have similar structures (I remember in Rust I've also seen those super-long inferred types).
And you shouldn't mind generally that the code is hard to compile, you should worry much much more that it will fail in the runtime. And those systems with more complex typing provide more guarantees that it won't happen. (and also other advantages that I'm not going to describe here)

3 Likes

@mfilonen2 Yea, I figured the general response would be, “You’re doing it wrong! You just need more composition!”

I’m a Mac developer. I have a 34-column Table. (Think iTunes.) The contextual menu for that table has 18 commands. I can promise you that it’s composed neatly of small little Views that have only the data they need, coupled with tight ViewModifiers etc.

(I will grant that iOS apps likely face fewer challenges because they’re simpler and they have far less complex Views and far fewer of them on-screen at any one time.)

I stumbled across the blog above and found it interesting that the guy who created the language now thinks the approach he took to type-checking was a mistake. I’m more interested in whether other prominent members of the core team share that opinion and think a course-correction might be wise.

You’re welcome to write me off as an idiot if you must. I did say it was a “swing for the fences” question.

7 Likes

@mfilonen2 I should also add that the problem is essentially limited to SwiftUI.

In “normal” code, the compiler does fine. Diagnostics catch obvious problems, etc. Speeds are good if you don’t rely on type inference.

It’s really SwiftUI where things fall apart—ESPECIALLY the diagnostics. Anything inside View is a prime candidate for the dreaded can’t type check this expression in reasonable time error. And often, as I’ve said, the problem is just a simple typo that the compiler would instantly flag in “normal” code.

5 Likes

Is there a way to configure how long the compiler takes before showing the “This expression is too complex
” message?

If possible I’d like to shorten the time for most development work, because as @bdkjones points out, often the problem is a simple typo that the compiler won’t/can’t help with, rather than being a gnarly type issue that it genuinely needs time to work out.

I tried searching through the compiler flags but couldn’t see anything that looked helpful.

4 Likes

I also had some troubles with a type checker, as every other Swift developer I think. However, in SwiftUI this problem occured to me only when the functions and properties were long and complex, and decomposition of them into smaller chunks always worked.
And of course there could be some improvements made, however, the proposal in a blog post is radical and also very doubtful.
Of course I am not a member of the Core team, but the idea to make a llanguage that already has a complex syntax even more bound to the specific editor with all of this features, making it less readable in every other places, like IDEs on other systems or Github or any other text editor, it just terifies me. It would mean also that the Swift files that are currently exclusively my responsibility (as a programmer), would be also modified somewhat semi-automatically by Xcode, which will also obscure its own edits by this proposal. XIBs are a good example of this approach – messy and unreadable files that are hard to maintain. And the alternative way of manual or semi-manual type writing would just increase the complexity of writing Swift code a lot.
The whole idea of a text format is that it can be viewed universally not just 'in Xcode, but in every other place and can be managed easily, and the code sources for every language I've seen are exclusively managed manually, IDE can provide some auto-indentation and that's all.

If you want a response exclusively from a core team member, that's fine, but you've written your proposal in a place that is made for a public discussion, so maybe you should've used other channels if you wanted to not hear other responses.

You could make the compiler warn you in this places and set specific time in milliseconds (in our codebase all warnings are treated as errors so effectively it was a compilation failure for us)

OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=80 -Xfrontend -warn-long-expression-type-checking=80";

2 Likes

you've written your proposal in a place that is made for a public discussion, so maybe you should've used other channels

I’m not proposing anything. I’m saying, “Everyone knows the type-checker’s performance is a longstanding issue. Are there any more ‘radical’ approaches that have been considered to solve it?”

It feels like Swift concluded the type-checker was “good enough” for AppKit and UIKit. Now that SwiftUI is largely supplanting those frameworks, the bottlenecks presented by the type-checker are a lot more impactful than they used to be.

And it’s not that I don’t want to hear from anyone other than core team members. You just came right out of the gate with, “You’re doing it wrong!” and assumed I’m just dumb. And maybe you’re right! But if so, I’m in good company because there are LOTS of developers who think the compiler’s performance in SwiftUI code is just not up to par.

5 Likes

Yes I do that, but it is after-the-fact — this will tell me which expression is causing a problem, but after a long wait (sometimes minutes). I’d rather know faster with a timeout of a couple of seconds (or fewer typechecker iterations)

The thing I totally agree with is that compiler has regressed in diagnostic a lot recently. Much often then before it fails to produce error in the right place even in 3-4 lines of code that is fully typed.

However, I don’t find this way to premise the problem as relatable:

If you have such issues with the compiler, probably code needs to be re-structured. SwiftUI (and basically any result builder, especially with generic return values) require more attention to that detail and the smaller parts you split them — the better is compilation and error-diagnostic. To be fair to the compiler here, SwiftUI got failed on type-checking from the first days of its existence if you get it too larger or something off. Decomposition of views here is the way to introduce type information for the elements which in other cases would require type-inference.


I think the issue that is more addressable is to improve diagnostic itself: it got much better since first versions of the Swift, yet got a lot of regress with the 6.x updates.

I’m a little confused what you’re proposing here. You suggest that SwiftUI is too difficult for the compiler (which is fair), and your solution is to require explicit types. But you admit that, at least as it currently is, it’s not possible to add explicit types in SwiftUI.

You also admit that the type checker is fine for “normal” code, so what you’re really suggesting isn’t that the type checker needs improving, but SwiftUI (and result builders) need improving.

1 Like

One thing I'm observing as of late is that, instead of getting the can't type check in reasonable time error when working with complex SwiftUI view hierarchies that contain an error (which at least would point me in the right direction), the compiler emits errors that turn out to be false (ie, claiming that a type involved in the expression does not conform to a protocol, even when it does).

Usually what I end up doing is a bisection search of the problematic view modifier by commenting out view modifiers in the expression until either the error goes away or the compiler emits the correct error.

I guess it would have been useful to open issues in GitHub when encountering such problems, but it only happens in complex hierarchies, so finding a minimal reproducible example is often an impossible task.

Don't think replacing the type checker or getting rid of inference is the right approach, though.

1 Like

I wish SwiftUI had its own, bespoke compiler. :slight_smile:

At minimum there SHOULD be an explicit type mode.

Or retooling.

I don’t use macros, deeply embedded generics, et cetera because I work with large projects and it truly leads to wasted time. Lots of it. It’s never worth the feature’s promises. Which is sad because they’re cool features .

I happen to have the latest machines when I want them, loads of excellent developers don’t. I can’t imagine suffering compiling on a 8GB - 16GB mac from over 4 years ago.

I think Swift has the potential to be a systems programming language.

It's based on counting operations and not measurement of wall time, but yes. Try passing -Xfrontend -solver-scope-threshold=NNN where NNN is an integer. The default value is one million.

15 Likes

As long as you're entertaining a "completely break the world of existing code" scenario, you don't even have to scrap the entire type system design and implementation. Simply getting rid of implicit conversions and type-based function/operator overloading would probably suffice.

7 Likes

Are there any plans to do that? I don’t think type-based overloads are a good idea. Even now, an API with type-based overloads will be slower to type-check and offer poorer diagnostics. Further, implicit conversions could be mitigated by having the migrator making them explicit. Finally, isn’t the fact that the compiler doesn’t automatically infer String on a string literal a problem, and how do you reckon we could handle operators if we remove type-based overloads?

Unfortunately it's not that simple. Imagine the annotation burden from having to explicitly specify all the simple stuff (class upcasts, erasing a concrete type to an existential) while also being forced to spell out function conversions (eg, dropping @Sendable from a long function type).

Polymorphic literals also introduce disjunction choices, like type-based overloading and conversions, but again you can't just get rid of them without drastically changing the language.

One possible approach is to say that there is just one global overload for each operator, but it's generic over a protocol. Rust does it this way for example: Operator Overloading - Rust By Example

A more realistic direction is to just make the constraint solver more sophisticated. There are many techniques described in the academic literature that we could attempt. The problem with the solver today is that while it is complicated (there are many language features to handle) the core strategy it uses is not very smart.

9 Likes

While there's likely work that can be done to improve the current implementation or changes to the language that could be mode to better limit the issues, I think one of the big things to do right now is improve the diagnostic language and fallback behavior of the compiler.

There are ultimately several issues with the "unable to type-check this expression in reasonable time" diagnostic.

  1. Like @Slava_Pestov said, this is no longer time based but instruction based, so it's inaccurate right off the bat.
  2. Given that devs newer to the language (or programming in general) are most likely to encounter it, the opaque language it uses is actively harmful. What is type checking? What is a reasonable time to do it in?
  3. There's zero suggestion on what to do. It seems like there could be several, if not many, notes the compiler could produce to suggest various changes, especially in cases like math or concatenation where a solution might be straight forward.
  4. There are no fix its, despite the compiler sometimes suggesting breaking the statement into sub statements. These seem workable, especially for simpler cases (probably not SwiftUI).
  5. The diagnostic can appear on a single line when the actual issue is a complex expression spread over many lines, or perhaps a whole closure. This is a more general issue with the diagnostic system, but really hurts here.

So a new message, with an actionable suggestions, or notes with suggested actions, which properly highlights the range of the expression, would be much more valuable.

More generally, Swift's default or fallback diagnostics have similar issues and are actively harmful when trying to fix compiler errors. In the cases where the true issue can't be identified, the compiler should work hard to ensure there are no false positives, such as the "doesn't conform to protocol" errors mentioned above. Just noting the manual actions to be taken when such a diagnostic comes up would be very helpful.

2 Likes

Thank you for the prompt reply!

I see. Do you think it's possible to make the type checker faster and more predictable even with implicit casting? I guess class upcasting isn't such a big problem since classes are, in my experience, the least used nominal type (especially now with non-copyable structs). As for existential/@Sendable casting, doesn't that usually occur in the following form (a binding or a function call)?

let boxed: any MyProtocol = myValue
myFunc(nonSendable: sendableFunction)

If so, isn't this relatively easy to type-check? I just can't come up with examples that would significantly complicate type-checking, but you might have something in mind given your expertise.

Could you elaborate on why that would be the case? Because, for instance, in the example mentioned in the blog, the only string-literal type that should be checked in my opinion is String since no other string-literal type is mentioned in the expression:

// why would we need to check that these literals are StaticStrings??
let url = "http://" + username 
            + ":" + password 
            + "@" + address 
            + "/api/" + channel 
            + "/picture"

So if I understand correctly, instead of checking both operands in binary operators, we'd just check the first one?

Could you cite some of these approaches or explain how the constraint solver could become more sophisticated?

Sorry for bombarding you with questions, but Swift's current approach to type-checking is very painful and I would really appreciate your insight!