Skip to content

Disfavor the TestPredicate-returning == overloads - #86

Merged
mansbernhardt merged 2 commits into
mainfrom
claude/disfavor-testpredicate-eq
Sep 25, 2026
Merged

mansbernhardt merged 2 commits into
mainfrom
claude/disfavor-testpredicate-eq

Conversation

@mansbernhardt

@mansbernhardt mansbernhardt commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The two public == operators in ModelTester.swift that return TestPredicate exist so that expect { a == b } failures can print both sides. Every file that imports SwiftModel can see them, though, and outside a builder they can win. A downstream app wrote let immediate = self.outcome == .needsNewPlayer in non-test code, it inferred TestPredicate, and it needed an explicit : Bool to compile.

Fix

  • Both == overloads are now @_disfavoredOverload, so an untyped comparison infers Bool. No other operator overloads return TestPredicate.
  • Keeping the builder's rich messages: disfavoring the operators alone caused a regression. The builder's buildExpression(Bool) was already disfavored, so each path carried one disfavored declaration, and the solver broke the tie toward the concrete synthesized ==. As a result, expect { model.outcome == .needsNewPlayer } lost its − Outcome.needsNewPlayer / + Outcome.pending diff. The fix removes AssertBuilder.buildExpression(Bool). A plain Bool now reaches the existing disfavored buildExpression(Bool?) through a value-to-optional conversion, and that extra cost keeps the tie going to TestPredicate. A comment at the overload explains why no Bool overload exists.

Verification

  • Two new builder … == … snapshots in OutputSnapshotTests cover the builder path with no type annotation. Before this PR, no snapshot covered it; the existing one used let pred: TestPredicate = …. The shapes are optional Int? == Int and an enum compared against an implicit member. I recorded both on main first, and the output with this PR is byte-identical on Swift 6.3.3 (Xcode 26) and 6.4 (Xcode 27). With only the operators disfavored, the enum snapshot fails.
  • Int == literal and String == literal resolve differently per toolchain, so they have no snapshot. On 6.3 they now show the rich diff, where main printed only SimpleCounter.count == 3. On 6.4 they still resolve to the concrete Int.==, as on main. Neither toolchain regresses. The first CI run failed only because I had pinned the 6.4 output for these two shapes.
  • TestPredicateOverloadTests uses a plain import SwiftModel. It assigns x == y to an untyped let and passes the result to a function that only accepts Bool, covering enum implicit-member, optional, struct and generic comparisons. It also checks type(of:) == Bool.self. The file fails to compile without the fix.
  • swift build --build-tests is warning-free. scripts/test passes: 721 main tests, 126 snapshot tests and the macro tests. The CounterFact, Onboarding, Search and TodoList examples build for testing. Standups fails inside the third-party swiftui-navigation checkout with the local toolchain, which is unrelated to this change.

🤖 Generated with Claude Code

mansbernhardt and others added 2 commits September 25, 2026 19:11
The public `==` operators returning `TestPredicate` (for `expect { a == b }`
diffs) leaked into app code: `let b = x == .someCase` inferred TestPredicate.
Mark both `@_disfavoredOverload` so untyped comparisons infer Bool.

To keep the builder's rich lhs/rhs failure messages, drop
`AssertBuilder.buildExpression(Bool)`: plain Bools reach the existing
disfavored `buildExpression(Bool?)` via value-to-optional, whose extra cost
keeps the tie between the two disfavored paths going to TestPredicate.
Without this, `expect { model.outcome == .x }` lost its diff.

Adds builder snapshots (Int, String, Optional, enum implicit member) pinning
the pre-change output, and TestPredicateOverloadTests (plain import) proving
Bool inference outside a builder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On Swift 6.3 (CI's Xcode 26.3) `expect { model.count == 99 }` now takes the
TestPredicate path and prints a diff (an improvement over the `count == 3`
baseline); on 6.4 it resolves to the concrete Int.== as before. Neither is a
regression, but they can't share one snapshot. Keep the optional and enum
implicit-member snapshots, which require the TestPredicate path and are
identical to baseline on both toolchains.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mansbernhardt
mansbernhardt merged commit d1b7ad8 into main Sep 25, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant