[Pitch] Narrowed Any

I don’t think this is my kind of feature so I don't want to rain on anyone who wants it, but throws(A | B) seems like it needs more: is there a mechanism to ensure that A and B each conform to Error if that erases to Any? Does it still use the typed throw codegen, which is intended to not allocate?

2 Likes

Well, that's not it. You've added at least 3 new features to the type system:

  1. Narrowing Any String | Int
  2. Narrowed Type Bounds
    1. T: NetworkError | DecodingError
    2. extension Array where Element == Int | String { … }
  3. Narrowing typed throws throws(NetworkError | DecodingError | AuthError)

What all three of these share is that they introduce this new kind of - admittedly bounded - disjunction into the type system. I am generally opposed to this as any form of disjunction forces the type checker to search*. For which, in particular, example 2.1 above requires.

I am also not convinced that the syntactic restrictions you've stated above are worth the restrictions in semantic power for a feature like this. If subtyping is completely off limits, why not make a set of macros that does simple injection at creation and checked casts for projection? As others have noted the type A | B is not just semantically - but even syntactically - sugar for

enum APlusB {
  case a(A)
  case b(B)
}

everywhere except typing bounds.

a compiler-generated enum has no place to hang the rule

Ah but it does! The Space Engine is quite clever about structurally uninhabited types.

enum Foo {
  case empty(Never)
  case full(String)
}

switch Foo.full("") {
case .full(let s):
  print(s)
}

One question I have for you about this proposal is: is the type T | Nominal supported where T is an archetype? I notice none of the examples have a general type as part of the structure

func foo<T>() {
  typealias Bar = T | String
}

It looks like you've implemented element-wise substitution so this ought to be supported.

Another is a note about the prototype: This

The constraint position is order-free: where T: A | B and where T: B | A accept the same set of substitutions.

Implies that you need to implement canonicalization for narrowed protocol types so that the signatures of

func foo<T: A | B>()
func foo<T: B | A>()

Are both properly rejected as duplicates and also mangle the same. I only skimmed your prototype but I didn't see where you were e.g. sorting the components. I'd point out too that if you're going to have this order-independence in just this one syntactic position why not extend it to all positions?

No OS upgrade, back-deploys to any Swift-supporting target

I want to be clear that it is really clever what you've done here, but I think it's important to call out that you have indeed added to the mangling which is going to require this back-deploy or, indeed, will require an OS upgrade to adopt the feature or the prior runtime will not be able to interact with these types.

*The most famous kind of semantic disjunct Swift has is in the form of overloading. I am also generally opposed to true negation (not the current ~ form, but true "Does not Conform to T") as this lets you encode !(A & B) which is equivalent to the search !A | !B.

2 Likes

I asked AI to help fix test failures, add more tests, and revise parts of the proposal.

This is a one-line-per-change index of what landed since post #20. Every entry links to the swift-fork commit (or swift-evolution doc commit), the proposal section it touches, and the test case where applicable β€” for design rationale on any specific item, jump to the proposal section linked.

1. Implementation completing earlier-announced design

2. Genuinely new since post #20

  • Disjoint cast as? / as! β†’ hard error in all three forms β€” unified across the proposal text (was previously a "warning" in three passages, "error" in the rule table). Swift fork ff2910fac5b + 4ee4d94f03d; proposal Β§"Cross-shape conversion".
  • Implicit cross-shape conversion β†’ hard error + auto-fix-it β€” let b: String | Int = a (where a: Int | String) used to silently succeed; now hard error with a leaf-set-classifying fix-it. Swift fork 4d0c3f9b4cc + 6bc151db04a; test .
  • Per-element leaf injection at the extension boundary β€” auto-fix-it (biggest change in this window). The cast itself was already wired end-to-end; what landed is the auto-fix-it inserting (receiver as [Int | String]) at the same-type-requirement error site. The implicit form is deferred β€” [Int] and [Int | String] have different per-element layouts (8 vs 32-byte stride), so it needs Path A (CSApply coercion) or Path B (SILOptimizer specialisation). Swift fork ae1bf61abce; proposal Β§"Containers and extensions" + Β§"Per-element leaf injection"; test .
  • Cross-spelling fix-it is a separate v1 limitation β€” zs: [Int | String] reaching extension Array where Element == String | Int goes through a different per-alternative path that doesn't carry narrowed-Any context, so the fix-it doesn't activate. The cast itself works.
  • Concrete cost numbers for the three reshape axes added to Β§"Containers and extensions": cross-spelling O(1) SIL relabel (runtime-free); leaf injection O(N) per-element wrap with ~4Γ— transient memory peak (8 β†’ 32-byte stride); narrowed β†’ leaf via as? is O(N) walk + dynamic check. The headline O(N+M) typed-throws composition argument depends on cross-spelling being O(1).
  • Library-author guidance added: prefer Sequence / Collection extension over Array-specific so users can pay leaf-injection lazily via xs.lazy.map { e -> Int | String in e }.method() for O(1) memory peak.
  • Tagged-union layout follow-up β€” explicit non-qualifier list in Β§"Tagged-union layout": non-POD leaves disqualify (String, URL, classes); all-POD sets exceeding the size budget still need padding.

3. Internal audit corrections

  • Scala-3 / "join" misattribution (post #4) fully resolved β€” visible-interface re-anchored as Swift's own LUB; comparison table no longer collapses Scala 3 / Ceylon / TypeScript under "Structural"; Maranget overclaim corrected. Swift-evolution e53c308 + 0f8ee10.
  • Five misframings / contradictions swept (join example self-contradiction; "only implicit conversion is leaf-introduction"; Codable Never encode try-order; OneOf3/OneOf4 framing; where T == A | B annotation qualifier). Swift-evolution d1e0308 + 534ff7d.
  • First-class type framing + extension priority model β€” Β§"It adds a new kind of type" reframed (first-class type-system identity + reused runtime existential machinery, dual nature like any P post-SE-0335); two extension dispatch rules anchored: receiver static type owns method-dispatch priority and user extensions take priority over synthesis fallback. Swift-evolution c9a5584.
  • Codable user-override extension Int | String: Codable { ... } restored as the canonical Issue 5 motivator across Β§"Issue 5" / Β§"Conformance synthesis" / Β§"Codable user override" / Β§"Extending a narrowed-. Swift-evolution 076e693.
  • Major correction: per-witness conformance synthesis is shipped in v1, not deferred. Stale text claimed Hashable / Equatable / Comparable / CustomStringConvertible / Encodable / Decodable were "Deferred from v1" β€” actually wired via 's BuiltinConformance(NarrowedAnyDispatch) + six SIL helpers in . Five proposal places fixed. Direct value-member access (v.description) is the only remaining piece, deferred to a focused follow-up. Swift-evolution 2005dc8.
  • Codable debugger-interaction note β€” the synthesised init(from:) invokes each leaf's decoder via try_apply, so Xcode's "Swift Error Breakpoint" fires once per failing leaf attempt during multi-leaf decode. Β§ Issue 5 now documents the diagnosis + four compiler-side mitigations (proposal commits to #1 SIL [suppress_will_throw] as a focused follow-up). Same audit caught all-fail decode shape: prototype propagates the last leaf's error verbatim (not DecodingError.typeMismatch annotated with the full leaf set as the proposal previously claimed); v1 commits to the prototype shape. Swift-evolution 54fb5f4.
  • Reverted a fabricated #ext-rule-namespace (22583f0 + 1b8fb51) β€” the supposed "leaf-typed receivers can NOT call narrowed-Any extension methods" was a fabricated problem; renamed to #ext-rule-leaf-reach with reversed semantics (most-specific-wins handles the apparent overload conflict).
  • Tagged-union spare-bit table β€” Β§ Tagged-union layout gained a 6-row table showing which leaf sets qualify (Bool | UInt8, Int | Double via NaN-boxing) vs. don't (Int | UInt32, Int | String). Swift-evolution f3890c8.
  • Per-element cost numbers correction β€” 32-byte stride (24-byte buffer + 8-byte metadata), not 24-byte; Int β†’ Int | String stride growth is 4Γ—. Swift-evolution b84c914.
  • Path A vs Path B framing for the deferred implicit lift β€” Β§ Per-element leaf injection split into Path A (whole-receiver coercion, no runtime change) and Path B (per-element specialisation, zero overhead for pure-iterate workloads).
  • Truncated-cell rendering bug fixed β€” Β§ Spelling is identity table had a Codable cell eaten by an unescaped |. Swift-evolution b47b802.

4. Test bed

Lit tests at . New since post #20:

  • (verify-mode) β€” disjoint-cast errors, cross-spelling extension dispatch, extension Int | String { } non-nominal note, implicit cross-shape as / as? fix-its, per-element leaf-injection auto-fix-it. Added 10372adb354 + Issue 9 / multi-clause-where in 2582b126015.
  • (runtime) β€” cross-spelling extension reshape end-to-end. Added 21ce1541660; fixed an earlier Sema ICE in matchDeepEqualityTypes (d0a5a54982e).
  • β€” per-leaf try-propagation, inhabited-subset throws(A | Never), throws(Never | Never) non-throwing.
  • phase3f_codable_struct_int_string.swift (runtime) β€” synthesised Codable round-trip on a top-level struct with Int | String field, 4 wire shapes Γ— both spelling orders. Added 9fede94cdc8; Β§5b (print(struct) field display) lock-in landed alongside the IRGen short-circuit in b5865b95a76.

5. Second-pass tightening (after the first draft of this update)

None of these change v1 design rules β€” they tighten v1 surface and shrink commitments.

  • Cross-spelling container assignment β€” full diagnostic + auto-fix-it. Used to crash in repairFailures; two-step fix (defensive fallback in getExistentialLayout + cross-spelling short-circuit in matchDeepEqualityTypes). Swift fork e1e2e9b55e2 + 7f56b39727b; test .
  • Cross-spelling fix-it β€” as! β†’ as (free relabel β€” as! over-states the cost). Swift fork c7e572f42f5; tests .
  • Cross-spelling at generic argument position β€” error + fix-it. Spelling-as-identity at constraint position; lifting to fully order-free leaf-set match is part of True set-membership. Swift fork c7e572f42f5; proposal Β§"Generics" + Β§"Issue 6"; test .
  • Per-leaf propagation extends to return position. func b() -> Int | String { return a() } (where a() -> String | Int) is value-flow and type-checks without an as. Swift fork ddf466c9da9; swift-evolution 310485c; proposal Β§"Return-position is per-leaf"; tests + .
  • Direct member access on a narrowed-Any value β€” moved to Future directions. Conformance is reachable through generic dispatch / interpolation / any-cast; v1 doesn't block any expressible code. Wiring Sema's value-member-lookup plausibly touches every overload-ranking site that already special-cases any P. Swift-evolution 55b643d; proposal Β§ Direct member access.
  • Constraint-position cross-spelling: corrected framing. v1 honours Spelling is identity everywhere a narrowed-Any is bound to a name, including constraint position. Real order-free leaf-set match parked in True set-membership. Swift-evolution fd3670b.
  • Codable last-leaf-error β€” committed to the prototype shape (see Β§3 Codable debugger-interaction). Swift-evolution 55b643d.
  • Test-bed + commit-message hygiene. CJK scan on commit messages and source files cleaned 4 commit messages and 2 source-file comments where workspace-side memo wording (Chinese phrases) had leaked through. Branches re-pushed; SHA references resynced.

6. Colon-form constraint banned in v1

The colon form where T: A | B reads as set-membership but the prototype delivers same-type binding β€” readers familiar with T: SomeProtocol would mentally model it as set-membership and be surprised. v1 commits to where T == A | B and parser-rejects the colon form outright; the colon-form syntax slot is reserved for True set-membership (which gives true set-membership semantics). Swift fork 8ae7cd938f7; swift-evolution 57d1482; new diagnostics Β§16 locks the parser-reject; full lit migration to == form. Compound T == A|B, T: Error from old Β§12k dropped β€” Swift already forbids T == X + T: Proto mix-ins, and Error is synthesised by the leaves.

7. Strict-invariant rule's silent passes

Β§ Subtyping lattice commits containers to invariant in element type; Β§ Variance at the protocol-witness boundary keeps function types strict-invariant (relaxation deferred). I noticed the constraint solver was letting four conversions through silently. Now an explicit "Known v1 gaps" entry β€” see :

  • let _: [Int | String] = xs for xs: [Int] (container leaf-injection at direct binding) β€” landed.
  • let _: [Any] = zs for zs: [Int | String] (container narrowing to Any) β€” landed.
  • let _: Set<Int | String> = s for s: Set<Int> (Set element widening) β€” landed.
  • let _: (Int) -> Void = f for f: (Int | String) -> Void (function-type parameter narrowing) β€” still silent, the remaining v1-blocking item.

Container axis fix is mechanical: in CSSimplify.cpp::simplifyRestrictedConstraintImpl's ArrayUpcast / DictionaryUpcast / SetUpcast cases, Type::findIf over either side's canonical tree forces inner element match to Bind; explicit as is exempted via the isExplicitCoerce idiom. Function-type axis fix lives in matchFunctionTypes. Swift fork 7f502492b87; swift-evolution 44f1a50 + 47e271e; new diagnostics Β§20; lit 13/13 PASS.

Spelling-is-identity is a landmine that I cannot personally accept. You're trying to co-opt punctuation / union-syntax which is commutative in other contexts.

String | Int == Int | String is a statement that must be true for me to accept a proposal. That issue alone makes it difficult for me to evaluate anything else in the proposal.

Further: Generics means that users may not have access to normalize the use-cases to any standard format, so you've made it impossible to benefit from this narrowing in tons of circumstances.

Please try again without that problem, or I'm opposed.