Replacing the Type Checker

Honestly, many other languages function perfectly well without full bidirectional type inference – I wouldn’t call it "handicapping". In fact, looking at Kotlin, it seems that fast and predictable type checking even unlocks something new: a different kind of expressiveness; a sense of unrestricted functional programming, where you’re not forced to break apart every other collection operation, and entire functions can often be written as single, concise expressions – something unimaginable with the current swift.

I’m also skeptical about the feasibility of significantly improving type checker errors, as many have requested. The current implementation often struggles to even arrive to a conclusion fast enough that there's an error at all.

2 Likes

I made a result builder for building UIView hierarchies. I don't remember encountering any timeout with it despite writing very large hierarchies as a single expression. Seems to me the compiler has an easy time dealing with result builders when everything is a UIView.

1 Like

I would say this is a fun combination of how SwiftUI creates complicated types that are largely hidden to the programmer. SwiftUI uses a lot of generics, some, any types. Whereas any result builder that creates UIViews would be dealing with just plain-ol classes with little to no generics and hidden types, the type checker preforms well.

SwiftUI pushes the type checker to its boundaries because its use of complicated types.

SwiftUI is being used as intended, it engages with features that the language provides.

Even though I despise SwiftUI and still avoid it, I can't come up with a reason that SwiftUI would be the problem here.

I also can't see why ResultBuilder would be implicated. It is also being used as designed.

This is largely an issue of SwiftUI using Swift as designed (generics in generics in generics with lots of type constraints) and the Type Checker not being able to handle it.

I am not sure that the type checker will ever be able to solve all valid cases in a reasonable time. So coming up with a guide on how to avoid hitting the boundaries of the type checker's capabilities would be nice -- until the type checker can be ameliorated.

I also think that programmers would do well to consider the constraints of the type checker as it stands today. There seems to be this mantra "well it should work so I am going to keep it they can just fix the type checker later" at play here -- which is bad for users of API. SwiftUI does offend in this regard.

Just some thoguhts.

2 Likes

Please don't interpret this as any kind of official advice because it is a very blunt hammer, and it doesn't apply to the case where your complex expression is composed entirely of calls to standard library functions.

But, if you find the type checker runs into trouble with your own APIs, the highest-impact thing you can do, by far, is to minimize overloading on names, including operators, as much as possible. It is a fact that overloading is "harder" than protocols, generics, deeply nested types, or almost everything else that one might intuitively guess contributes to slow type checking. This "hardness" extends to the difficulty of generating diagnostics -- most of the problematic cases involve overloading where neither choice leads to a clearly better outcome, both are just invalid in different ways, and the type checker has to make a judgement call about what to diagnose.

For example, say you have a bunch of commonly-used “currency types”, and each one has several overloaded inits, that all share the same argument label and thus have the same name, that take an Int or a String or any other number of types:

struct S1 {
  init(_: Int) {}
  init(_: String) {}
  ...
}

struct S2 { ... more inits ... }

And then you have an expression like:

foo(bar(S1(...), ...), S2(...), baz(S3(...)), ...)

Now, it is important that not every such expression will take exponential time to type check, far from it---but maybe foo() and bar() have complex overload sets as well, literals are involved, and some other part of the expression is complex in some way or even invalid. In this scenario you're far more likely to encounter exponential behavior.

Making judicious use of argument labels, or even replacing multiple concrete overloads with a single generic function that extracts the common behavior into a protocol somehow, can help.

This obviously isn't possible with operators, but as an API designer, it is also worth considering if a descriptive method name might even be better in some cases than overloading some operator.

Sometimes we see reports of slow type checking related to operators where, eg, the project introduces a public overload of func +(_: CGRect, _: CGPoint) or something like that, with two "unrelated" concrete types. I certainly cannot fault anyone for solving a problem in front of them using the tools provided by the language --- but it is worth keeping in mind that at least with the current design of operators, this sort of thing would be extremely difficult to make fast, and such overloads can even slow down unrelated expressions that happen to involve those operators.

(Something we've discussed in the past is possibly changing the design of operator overloading to fix some of these non-local performance effects, but I believe any such change would go in the direction of completely banning such "bespoke" overloads among unrelated types, rather than making them fast.)

18 Likes

I would love to test a special mode that disables all bi-directional heroics. I guess I'll have to use var v: Float = Float(1.0) and ExplicitType("literal") + ExplicitType("literal") everywhere so it's a tradeoff, but on the bright side would be lighting fast to compile!

3 Likes

Something we've discussed in the past is possibly changing the design of operator overloading to fix some of these non-local performance effects, but I believe any such change would go in the direction of completely banning such "bespoke" overloads among unrelated types, rather than making them fast.)

Sounds good! Please let the door hit overloads on their way out. Violently and repeatedly.

1 Like

Thanks for these tips, Slava.

It seems the OP's complaint is aimed mostly at the "can't type check in reasonable time" message and these seem more commonly a problem in code using result builders. If it is even possible, how can we apply this advice to code like this?

My question is really why in the world does the type checker have such a poor design inside the compiler? The type checker should never defeat syntactic analysis and should never have any effect on shift/reduce functionality of a basic recursive descent parser. Yes, there are things about the type system that affect code generation, but that should not be part of creating an AST to completely document the “tokenized” structure of the program. Only as an AST is traversed for code generation should the type system cause effects that would then result in type use errors being reported. The AST structure should document expression/line/token #/space details about what the node is about. Code generation should then be able to report exactly what’s wrong with the type usage.

But, it would appear the compiler and type system interactions is a disastrous cluster FK of code structure that is allowing the type system to create syntactic processing stalls.

I think that the most common problem I have is related to type incompatibility when substituting a protocol for an struct and the protocol not having the same field name as the struct. It should be possible to do this, and be told about this specific “no such field“ error readily. But, instead, inevitably a surrounding loop traversal in a Form or Stack will just report some random selection of possible type mismatch error messages about “C” not having an appropriate type.

Why in the world would Apple let such nonsensical software out into the world? What do you guys actually test with? It would seem that you are only testing with correct software and not attempting scenarios of change to a codebase that developers would typically go through, such as the conversion of struct use to protocol as software is turned into a library needing to support multiple types of objects.

In particular, I have software with Station and Contact structs. These are being converted to use StationInfo, EventInfo and ContactLocation protocols. These two structs will use 2 each of these protocols, and as I am making these changes, the world of compiler errors should only be about missing/incorrect field names. But instead, the errors are all kinds of nonsensical errors about loops and other related type confusion leading to compiler stalls creating vague, messages that keep me from being able to see what simple fixes I need to make.

I've noticed that you've made assertions like this a few times before, since I've tried to explain otherwise. But if you're going to criticize the design it's probably important to have the right understanding of the design.

As in most modern compilers, parsing in Swift is a completely independent and earlier pass from type checking. There is no interaction between the type checker and the parser like what you're describing. The compiler generates a parse tree (purely syntactic), produces a semantic AST from that (which retains the source location information from the syntax tree), and type-checking and code generation operate purely based on the semantic AST.

Don't get me wrong—there's still legitimate work to be done (and much that has already been done!) to improve the performance of the type checker. But it has nothing to do with shift/reduce conflicts or parsing. It doesn't help the discussion to claim the design is something that it's not and then call that design "nonsensical".

39 Likes

Xcode

Swift is open source and any experience (or not) developer can contribute and improve it.

3 Likes

to be fair, the rebuttal kind of hides the fact that the syntactic AST just isn’t all that “useful” compared to its counterparts in other languages, it’s not even that useful for syntax highlighting beyond the most basic of coloring features. notably, operator expressions are unparseable without having full knowledge of a module’s build tree, because you need to know the precedence and associativity of the operators. so we can say that parsing and typechecking are properly-separated layers in Swift, but that comes with the caveat that Swift “parsing” is so sophisticated that it might be comparable to “typechecking” in other languages.

3 Likes

I’m putting this here for people that are smarter than I:
Their prototype handles cases that timeout Swift 5.8 (~9 min) in 66ms.

11 Likes

Final Answer: Just Use Claude.

Xcode 26.3 is now live, which brings integration for AI agents. The default, ChatGPT, isn’t too great at solving type-checker failures, but Opus 4.5/4.6 is.

Just sign up for an Anthropic account, stick Opus in Xcode, and anytime the type-checker falls over, tell Claude to fix it. It’ll find the typo in a few seconds or refactor the too-complex expression. It’s actually astounding.

Opus is the smarter type-checker I proposed at the beginning.

1 Like

Cool. This line of thinking is why Chrome tabs take up infinity RAM and nobody at Google cares to do anything about it, btw.

8 Likes

Ha! Look, I just want to get work done. If an AI can spot the tiny typo that brings the type-checker to its knees, I’ll take it.

Long-term, Swift is dead. (As is every other higher language.) These languages exist for humans. In another decade or so, the LLMs will just generate assembly or machine code directly and everything in between English and CPU instructions will just melt away. I know that seems impossible right now, but it’s coming.

Think of it this way: today, you write Swift and you trust LLVM to just do the right thing for all the stuff below Swift. Turn it into whatever the CPU needs. You never pop open the compiled output in a hex editor. Tomorrow, you’ll trust an LLM to handle everything below English.

So in a way, it doesn’t really matter if Swift improves here.

Ah, yes: English, the famously unambiguous language. Surely all problems can be expressed succinctly and unambiguously in it.

17 Likes

Firstly, I’m not sure if you’re suggesting that we would simply prompt an LLM and it would produce machine code, and then later if you want a new feature you prompt again and it alters the machine code. It seems clear to me that this is not where development is going.

Maybe instead you're suggesting that the code base would be written in English, and "compiling" it would involve the LLM converting the whole thing to machine code. This sounds a bit more realistic to me, at least compared to the first option, but still I doubt that that's how things will shape up.

To me it makes much more sense to think of LLMs as yet another layer on top of the stack that we already have. The models themselves will probably get at least a bit better, and the tooling/integrations of LLMs into our workflows will surely improve dramatically, but I think the fundamental structure of development will remain more or less what it has become now, namely that we prompt LLMs to produce high level code (e.g. Swift), and for simple/extremely well-established things you don't even need to look at the generated code, but many other things will not be possible to achieve without looking at and understanding the code so as to be able to tweak it yourself or prompt the LLM further with more specific and technical instructions.


Edit: Actually, I think it's not quite right to consider the LLM as part of the "stack" at all, because its role is different. The "stack" is comprised entirely of "programming languages", which is to say simply that they are unambiguous. There is always room (in theory) to add yet another, even higher level programming language to the stack. What LLMs do is that they make the leap from ambiguous human language to the current top of the stack. The stack is incredibly useful here, because the stack makes that leap smaller, and therefore much more reliable. An even higher level programming language added to the stack would shrink the gap even further, making LLMs even more reliable at representing the prompter's intent in code. So, rather than the stack becoming obsolete, I think that, on the contrary, continuing to build up the stack to even higher levels will make LLMs even more useful.

2 Likes

You could say that improved LLMs will replace the IDEs we currently use, but I substantially agree that LLMs will never replace all the tools that translate our intent into instructions the CPU can faithfully execute.

1 Like

What you’re describing is where we are today. And the picture you’ve painted is accurate.

But the only reason Swift (or any language, for that matter) exists is because humans need an abstraction layer. We can’t read and reason about a stream of op codes. So we built giant chains of tooling to translate symbols that we can reason about into the op codes that a CPU eventually executes.

I’m simply suggesting that tomorrow’s LLMs will be perfectly capable of reasoning about codebases without the higher-level abstractions that humans require. Because eventually LLMs will be able to reason about a giant stream of op codes, like you and I reason about Swift code today.

That’s not on the immediate horizon, but it’s coming.

And with that, I’ll excuse myself from the debate. I’m not really interested in it and came here merely to suggest Opus in Xcode 26.3 as a “better type-checker when the real one fails.” Until a few months ago, when I finally started using Claude, I was squarely in the “veteran programmers will always have a role to play; no AI will replace us and our tools!” camp, so I understand the reaction.

LLMs fundamentally cannot reason. They randomly generate data that's shaped like their training data. That's not a limitation in the current implementations, that's the only thing an LLM does. Any reasoning system would be entirely seperate from the LLM, and once you have a reasoning system fast enough and reliable enough to use for this purpose, the LLM itself becomes redundant. LLMs aren't the future, they're barely the present.

10 Likes