Replacing the Type Checker

In fact no string-literal types are “mentioned” in that expression.

My understanding is that the type-checker does first try using the default types for literals, which in this case would be String. If that works, then it’s done.

But the problem is, in the example, that doesn’t work. There are no implementations of “+” that will make the expression type-check with the literals all inferred as String.

So now the compiler says “Okay, I guess it doesn’t type-check with the default literal types, but there might still be a way for it to work if some of those literals are actually a different type.”

In order to rule that out, it has to try every possible combination of types that are expressible by string literals, and see if there are any possible overloads of the + operator that will make the whole expression valid. That clearly has exponential complexity, because each literal and each operator could potentially be any of the possible options.

So really the question ought to be, “What information does the type-checker need, in order to determine that it doesn’t have to try every possibility for every literal and every operator in the expression, and how can we convey that information to it?”

8 Likes

Thank you Slava , that sounds like exactly what I was hoping for! I’ll experiment a bit and see if there’s a configuration that works better for me.

Exactly! I think Slava’s idea of global operators generic only over the protocol might help cut down on the choices here. Because, if operators are treated as method calls on the first operand, I think type-checking would be drastically simplified:

let url = "http://".add(username).add(":")./* ...*/.add(channel)

Here, .add(channel) should immediately give the error that + expects a string and not an integer, potentially offering a fix-it: String(channel).

This formulation where operators map to instance methods has a different problem, in that information only flows from left to right. We cannot begin solving a member reference expression until we know enough about the base type to perform a lookup, because until then we don’t know the type of the member.

I think a hybrid approach where operator lookup is global, but each operator is in its own protocol, would be fastest to type check. That might be too restrictive in practice though.

2 Likes

YES!

Improving compiler speed would be nice, but if I could just get actually useful diagnostics in SwiftUI code, that would be 80% of the battle.

Everyone assumes I have Views that are 8,000 lines long and I’m asking unreasonable things of the compiler.

That’s not the case. I’ve spent a lot of time optimizing and composing Views. (You must do that to make SwiftUI anything close to performant for a complex Mac app with many elements on screen at once.)

But when I have a simple typo caused by an incorrect auto-complete of withID: instead of forID:, that’s the sort of thing I expect a compiler to catch and flag. And Swift’s compiler DOES do so, as long as you’re not in a SwiftUI View. Once you are, all bets are off.

It seems like the core team building the language might not spend much time making SwiftUI apps (which is expected and reasonable; building Swift is a full-time job), so maybe they aren’t really aware of how bad things have gotten there?

My current coping strategy is to write a couple lines and then build to see if the compiler works. If you build only occasionally, you’re likely to see the can’t type check in reasonable time error and then have to backtrack to narrow down the exact problem, which is usually quite trivial.

10 Likes

You probably can have an opt-in mode that is per-function or per-file for iterating on type inference.

2 Likes

Maybe a philosophical way to phrase the question that’s better:

If you were designing the successor to Swift from a clean-sheet, would you choose the same type-checking approach? Or would you go a different route now that we see the performance bottlenecks in modern frameworks like SwiftUI?

If the answer to the above is, “we’d make some different choices,” then the natural follow-up is: can we evolve Swift toward those choices?

13 Likes

Personally, I'd be willing to give up quite a bit of other conveniences to get better diagnostics and get rid of that crazy "took too long" error. Would give up the concise enum syntax, polymorphic literals, and function overloading. Is that enough?

4 Likes

Here's a perfect example I just hit! Can you spot the error?

parentCueSheet needs to be CueSheet? but I typed CueSheet (omitting the ?). Stupid-simple problem, but the compiler is completely flummoxed. Yet if you make this same mistake outside of a SwiftUI View, the compiler traps it instantly and tells you precisely what's wrong.

THIS is the issue. THIS is what I fight day in and day out. Ticky-tack stuff like this. The compiler is often extremely frustrating in SwiftUI and completely unhelpful. That murders productivity. And as more apps transition to SwiftUI, more and more developers are going to hit this.

So maybe it's not a wholesale replacement of the type-checker, but there must be some levers to pull to make the compiler more useful in these situations?

Composition

This Table is really simple. Just a handful of straightforward columns:

This is a case where "just split it into subviews!" doesn't make sense. The columns have to go in Table. The .contextMenu(forSelection:) modifier has to attach to the Table. It has one Button. This isn't a 7,000-line View with 34 different if branches.

AI Results

Just as an interesting aside: no AI could find the exact problem given the code and a prompt that included the diagnostic message, the line on which it appeared, and a hint that there was a single typo in the .contextMenu(forSelectionType:) modifier. They did, however, back into a solution by rewriting the modifier without explicitly declaring types for the two lets.

15 Likes

I agree that the compiler is really not good enough at handling type errors. I also think people are way too quick to excuse the compiler by claiming your view body shouldn’t be that big. Perhaps it would help if Swift had a way to specify that a closure should be a type checking boundary, without requiring us to factor it out into a separate method.

That said, you can factor out your table columns. You can create a type that conforms to TableColumnContent in much the same way you can create a type that conforms to View. You can also create a property or method that returns some TableColumnContent<MyRowValue, MySortComparator> like you can return some View. It’s more cumbersome with columns than with a view because you have to specify the row value and sort comparator types, but it’s quite feasible. Would it help the compiler in your example? I don’t know.

3 Likes

That said, you can factor out your table columns.

Yea, I know. But when the Table has just 7 columns and each one is a single Text, it’s a bit crazy to do that. I think this Table falls well within the definition of a “reasonable” View. It’s a pretty typical one that any Mac app might display: a single Table and its assigned ContextMenu (which has just one command).

There’s a balance to be found between 5,000-line Views and projects that contain 5,000 View files, each with just 20 lines. Neither extreme works.

5 Likes

I think polymorphic literals are a big part of the problem and should be deprecated.

They behave like implicit constructors in C++ — something we frown upon because they hide conversions and make the type system less predictable.

In Swift, literals like 2 or 2.0 “magically” adapt to context, but that slows down the compiler and hides type information from the reader.

It’s also incoherent: when you replace a literal with a variable, you often have to rewrite code because the literal wasn’t properly typed.

let result = 3.5 * 2  // fine: 2 is magically a Double

let two = 2
let result = 3.5 * two  // error: no implicit conversion from Int to Double

The literal 2 adapts silently, but the variable two does not.

This surprises newcomers, makes code less uniform, and hurts compile times.

We should push for explicit typing of literals just like we demand explicit constructors elsewhere.

9 Likes

Yet explicit required typing in Swift as you initially suggested at the beginning would either require you to factor-out columns or made result builders and SwiftUI in the current form impossible.

In case of the Swift to require you explicitly type a lot of things and support result builders, you'll have to factor-out columns into a separate property to explicitly specify type of content and key-path. Apple even has an example of doing so in the documentation:

@TableColumnBuilder<Person, KeyPathComparator<Person>>
private var nameColumns: some TableColumnContent<
    Person, KeyPathComparator<Person>
> {
    TableColumn("First Name", value: \.firstName) {
        PrimaryColumnView(person: $0)
    }
    TableColumn("Last Name", value: \.lastName)
    TableColumn("Nickname", value: \.nickname)
}

That's a complex expression which embedded directly in the Table columns property has to infer key-path (with is a hard task for Swift to infer in the first place). By simply moving columns into a property (not a file or even a view) you have explicitly typed value – which is what you've been asking in the first place.

1 Like

Yep, I know.

I’m not arguing for a specific approach. I asked if any radical departures had been considered and gave an example. (Then later clarified that as: “if we were designing Swift from scratch today, would we choose a different approach for type-checking?”)

The constraint solver is bursting at the seams handling reasonable SwiftUI Views. It’s been listed as a known issue for years and that issue feels like it has languished. Small tweaks haven’t improved the situation. (In fact, everyone agrees that diagnostics have gotten worse.) So is there an entirely different approach that could better fit the modern ways that Swift is used?

Frankly, even just convincing the team that this issue is far worse than they think and that it should be a higher priority would be a win. It just seems like they know that, but the type-checker is fundamentally limited, we’ve hit the ceiling, and nobody knows how to get out of this corner.

8 Likes

I assume "instruction count" is a deterministic measure. So either the code always compiles, or the code never compiles. A time-based cutoff, on the other hand, could work on some computers but not others, or even fail non-deterministically on the same computer based on load. Since it's an arbitrary threshold anyway, it's probably best to choose something that is not subject to randomly change across runs.

6 Likes

I’m a bit confused about the reluctance to the “break most of the existing code”. We’re currently doing that with Swift 6/Strict Concurrency mode. And I don’t really think most app developers really wanted/like that, (as evidenced by the latest vision document) unlike replacing the type checker which is a well known problem.

8 Likes

Can you elaborate a bit on how much upside you see? Are we talking “if we invest in making the constraint solver more sophisticated we’ll see a small improvement around the edges” or is it “with modern techniques we can make the constraint solver dramatically more performant?”

And I think the broadest reading of the OP is “does the core team see this problem and consider it a priority for investing effort?” I don’t feel like that question has been answered.

13 Likes

It's SO aggravating! But still...

The Swift type checker design might not be the kind of thing that's advanced via informal discussions. Nor is the pain really addressed by telling developers to refactor their code. Nor is it anywhere near likely that Swift should break language features just to avoid type-checking tarpits.

A more fruitful path could be to find ways to improve how developers can address the failure when it arises, particularly if there are small changes to type checker controls or diagnostics that could help. So perhaps the discussion could fork off in that direction?

AFAICT, the current state of the art for developers addressing these errors is

  • Use the flag to reduce the operations tried before halting
    • -Xfrontend -solver-scope-threshold=n (n defaults to 1M)
  • Edit code to isolate the problem, esp. with some awareness of high-traffic/impact problems:
    • Type-specific (global) operator overloads
    • Deeply nested some-type inference (in SwiftUI)

Moving forward, it would be nice to known if it's possible for the type checker to detect and stop or warn when:

  • when function parameter names matches no candidate?
  • when n some type combinations are being considered?

Another approach to aim for more first-failure data capture that permits some diagnosis without further experiments. Right now, the error message is simply that the expression can't be checked. Is there some way to accumulate some of the ambiguities and list them when giving up? (Given the memory/housekeeping cost, only as a special diagnostic mode?)

One way to find such cases is for Swift developers to report bad code and how they ended up fixing the problem when the type-checker balks, so the type-checker developers can see whether there's some fast-fail or early-warning possible. Is there an issue-reporting flag on point?

4 Likes

To be fair, the example provided, in my opinion, is "Sh***y code" in the first place.

Thanks. Would you care to descend from your mountaintop and share your brilliance? Because the example I gave is basically as simple as SwiftUI Tables get, unless you’re an iOS developer. It’s a few columns of text and a one-button context menu. By all means: show me how to do it, my man.

12 Likes

Of course it's "Shi***y code." It contains a bug which the compiler can't find quickly enough. And neither can I.

5 Likes