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?
Well, that's not it. You've added at least 3 new features to the type system:
- Narrowing Any
String | Int - Narrowed Type Bounds
T: NetworkError | DecodingErrorextension Array where Element == Int | String { β¦ }
- 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 | Bandwhere T: B | Aaccept 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.
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
- Per-leaf try-propagation (post #13) β wired end-to-end. Swift fork
93368136543; proposal Β§"Try-propagation is per-leaf"; test covers @ksluder'sdoBothThings. - Inhabited-subset rule for
Neverleaves (post #15) β wired end-to-end. Swift fork93368136543+c92aeec4495; proposal Β§"Uninhabited (; tests cover @Nobody1707'sErr<Never, Never>.
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 forkff2910fac5b+4ee4d94f03d; proposal Β§"Cross-shape conversion". - Implicit cross-shape conversion β hard error + auto-fix-it β
let b: String | Int = a(wherea: Int | String) used to silently succeed; now hard error with a leaf-set-classifying fix-it. Swift fork4d0c3f9b4cc+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 forkae1bf61abce; proposal Β§"Containers and extensions" + Β§"Per-element leaf injection"; test . - Cross-spelling fix-it is a separate v1 limitation β
zs: [Int | String]reachingextension Array where Element == String | Intgoes through a different per-alternative path that doesn't carry narrowed-Anycontext, 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 headlineO(N+M)typed-throws composition argument depends on cross-spelling being O(1). - Library-author guidance added: prefer
Sequence/Collectionextension overArray-specific so users can pay leaf-injection lazily viaxs.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 | Bannotation qualifier). Swift-evolutiond1e0308+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 Ppost-SE-0335); two extension dispatch rules anchored: receiver static type owns method-dispatch priority and user extensions take priority over synthesis fallback. Swift-evolutionc9a5584. - 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-evolution076e693. - Major correction: per-witness conformance synthesis is shipped in v1, not deferred. Stale text claimed
Hashable/Equatable/Comparable/CustomStringConvertible/Encodable/Decodablewere "Deferred from v1" β actually wired via 'sBuiltinConformance(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-evolution2005dc8. - Codable debugger-interaction note β the synthesised
init(from:)invokes each leaf's decoder viatry_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 (notDecodingError.typeMismatchannotated with the full leaf set as the proposal previously claimed); v1 commits to the prototype shape. Swift-evolution54fb5f4. - Reverted a fabricated
#ext-rule-namespace(22583f0+1b8fb51) β the supposed "leaf-typed receivers can NOT call narrowed-Anyextension methods" was a fabricated problem; renamed to#ext-rule-leaf-reachwith 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 | Doublevia NaN-boxing) vs. don't (Int | UInt32,Int | String). Swift-evolutionf3890c8. - Per-element cost numbers correction β 32-byte stride (24-byte buffer + 8-byte metadata), not 24-byte;
Int β Int | Stringstride growth is 4Γ. Swift-evolutionb84c914. - 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-evolutionb47b802.
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-shapeas/as?fix-its, per-element leaf-injection auto-fix-it. Added10372adb354+ Issue 9 / multi-clause-where in2582b126015. - (runtime) β cross-spelling extension reshape end-to-end. Added
21ce1541660; fixed an earlier Sema ICE inmatchDeepEqualityTypes(d0a5a54982e). - β per-leaf try-propagation, inhabited-subset
throws(A | Never),throws(Never | Never)non-throwing. phase3f_codable_struct_int_string.swift(runtime) β synthesisedCodableround-trip on a top-level struct withInt | Stringfield, 4 wire shapes Γ both spelling orders. Added9fede94cdc8; Β§5b (print(struct)field display) lock-in landed alongside the IRGen short-circuit inb5865b95a76.
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 ingetExistentialLayout+ cross-spelling short-circuit inmatchDeepEqualityTypes). Swift forke1e2e9b55e2+7f56b39727b; test . - Cross-spelling fix-it β
as!βas(free relabel βas!over-states the cost). Swift forkc7e572f42f5; 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() }(wherea() -> String | Int) is value-flow and type-checks without anas. Swift forkddf466c9da9; swift-evolution310485c; proposal Β§"Return-position is per-leaf"; tests + . - Direct member access on a narrowed-
Anyvalue β 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-casesany P. Swift-evolution55b643d; proposal Β§ Direct member access. - Constraint-position cross-spelling: corrected framing. v1 honours Spelling is identity everywhere a narrowed-
Anyis bound to a name, including constraint position. Real order-free leaf-set match parked in True set-membership. Swift-evolutionfd3670b. - 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] = xsforxs: [Int](container leaf-injection at direct binding) β landed.let _: [Any] = zsforzs: [Int | String](container narrowing toAny) β landed.let _: Set<Int | String> = sfors: Set<Int>(Set element widening) β landed.let _: (Int) -> Void = fforf: (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.
As an aside, all other languages with sum types of which I'm aware would treat these as equivalent (e.g. by automatically sorting the inner type list at compile time.)
Typed throws is the killer use case
Union-like types are so commonly pitched for typed throws, but do we really need it for multiple typed errors?
Forum search is terrible so correct me if this has been said before, but (edit: it's in the original proposal) with the constant denial of union types (which this proposal is) it seems like we can only get multiple typed throws through the type checker rather than forcing a new type.
Comma separated throws list doesn't look too unfamiliar from all other declaration lists in the language, whereas a | would be.
func foo() throws(ThisError, ThatError) { /* ... */ }
do {
try foo()
} catch error as ThisError {
} catch error as ThatError {
}
This would require error type exhaustive checking to be implemented, which I think is far more motivational for correctness' sake.
No exhaustion today?
This only errors at non-top-level code.
struct ThisError: Error {}
struct ThatError: Error {}
func foo() throws(ThisError) { throw ThisError() }
func bar() throws(ThatError) { throw ThatError() }
do {
try foo() // Error: Errors thrown from here are not handled because the enclosing catch is not exhaustive
try bar() // Error: Errors thrown from here are not handled because the enclosing catch is not exhaustive
} catch let error as ThisError {
print("Caught \(error)")
} catch let error as ThatError {
print("Caught \(error)")
}
Yeah, to my mind, a feature like this that doesn't enable exhaustive checking just isn't worth it. If I was okay with that I'd just throw any Error.
I can't speak to the pitch as a whole. It's very hard for me to trust the proposal text given its revision history (not because of the use of LLMs per se, but because the way they're being used makes it very hard to judge the epistemic grounding of the proposal text), and I'm not competent to evaluate the implementation.
Taking it at face value however, and focusing on the one small part I have strong opinions about: I don't think an untagged Codable round trip is wise.
Decoding is by default lenient (happily ignoring unknown keys), so in A | B if an encoded B happens to decode successfully as an A the round-trip will change the type of the value, even if this means dropping some of the data that B needed but A does not. Imagine encoding a mixed array of Point2D and Point3D values, and getting back only Point2D values having silently dropped the z coordinate data. Silent data loss is a nasty failure mode.
While I fully agree that (A | B) unions are highly needed for typed throws , I believe we should require strong motivating examples before introducing this syntax in other contexts.
For instance, a general Int | String union doesnβt seem sufficiently justified to me. In many years of development, Iβve never truly needed something like Int | String β every such need has been perfectly addressable with generic enums like Either /OneOf , or with a dedicated enum for a specific API.
That might just be my own bias, but before expanding sum types beyond error handling, I think that:
- compelling motivation for their use elsewhere should be provided
- or be narrowed to typed throws
Since the feature itself is quite debatable, I'd suggest we focus on introducing it for typed throws initially, and move introduction it as a general feature to separate pitch.
It's not in Haskell, and to me either A|B equal or not equal to B|A is not a show stopper either way. FWIW, a tuple could be considered a counterpart product type for which (A,B) != (B,A)
@fclout
Error conformance: each leaf in throws(A | B) is required to conform to Error, checked at the throws position by Sema β same check that already gates throws(SomeError) on SomeError: Error. A non-Error leaf at the throws position is a hard error with a tailored diagnostic; mixed-conformance leaf sets are rejected up-front, not deferred to the runtime.
Codegen: yes, SE-0413's typed-throws lowering is reused as-is. The throws slot is sized for the existential layout (24-byte inline value buffer + 1-word metadata pointer = 32 bytes total), not heap-allocated by the throws ABI itself. Allocation behaviour for the leaf value follows the existing any P / any Error storage convention:
-
All-POD leaves whose largest member fits the inline buffer (typical error-enum codebase:
Int | UInt8 | Bool, or a set ofenum Foo { case a, b }payloads): no heap allocation, the value inlines in the slot β same shape as aResult<T, ErrorEnum>with a small enum. -
Non-POD or oversized leaves (
String,URL, classes, large structs): the existential heap-allocates as it does for anyany Errorstorage today. SE-0413 has the same trade-off.
The Tagged-union layout for small POD leaf sets follow-up explicitly targets the all-POD case to remove even the existential overhead β turning the throws slot into a sized-tagged union (no metadata pointer at all). That follow-up is the path to "as cheap as a hand-rolled wrapper enum" for typed-throws error sets, and is the load-bearing path for Embedded Swift adoption.
So: yes the conformance check is real; yes the codegen reuses SE-0413 without giving up the no-allocation property for the leaf-set shape that already qualifies under existing existential storage rules; the Tagged-union follow-up closes the gap to the hand-rolled-enum baseline for the all-POD case.
@codafi β thanks for the substantive review. Going point by point:
three features, all forms of disjunction forcing type-checker search.
The three syntactic positions share one underlying primitive: a closed conformer set on the existing any P existential layout. The constraint solver doesn't search over the leaf set β the set is fixed and finite at the point the alternation is written, and every comparison reduces to a linear scan over a known list. There is no constraint-disjunction in the solver-search sense (no or-arms backtracking, no exponential branching). The exhaustiveness checker is Maranget pattern usefulness specialised to a sorted leaf-set, and cast feasibility is leaf-set intersection β both decidable in linear time on the leaf-set size. The design is specifically scoped to avoid introducing the search machinery that true negation / true disjunctive bounds would.
macros + a hand-rolled
enum APlusB
A macro can emit the wrapper enum and inject leaves at creation, but it can't compose at the type level. The cross-library O(N+M) story is the load-bearing benefit: two libraries that both want (NetworkError | DecodingError) resolve to the same type under this proposal, and a consumer composes their throws sets without writing a third wrapper. A macro-emitted enum APlusB mangles per-library and per-expansion-site, so the consumer is back to wrapper-enum-of-wrappers O(NΓM) β exactly the friction the pitch is trying to remove. throws(A | B) as a macro return type doesn't exist as an idiom either; macro expansion happens before type-checking sees a return-type position, so the call site can't be told "this throws an A | B" through a macro.
sugar for
enum APlusB { case a(A); case b(B) }everywhere except typing bounds.
Partially true at the value-layout level β runtime is morally one-of-two with a tag. The differences that make it not sugar:
- Cross-library type identity :
enum APlusBmangles per-library;A | Bshares identity across libraries. (This is the headline argument.) - Leaf injection is implicit :
let x: Int | String = 7, where the enum requires.a(7). - Codable convention follows declaration order with no user-invented case names to bikeshed.
- Witness selection routes through per-leaf conformance synthesis for the six stdlib protocols (details) β
Set<Int | String>,JSONEncoder().encode(v),<overInt | Double,\(v)interpolation all work without the user implementingHashable/Encodable/ etc. on a wrapper. - Pattern matching uses
case let _ as T:, which is the existingAny-open-the-box idiom rather thancase .a(let v):β no user-invented case names appearing in catch arms either.
On Space Engine + uninhabited cases. Yes, enums already have switch exhaustiveness checking, a more accurate way to put it is call-site reachability; my earlier wording was off.
throws(A | Never)should requiretryexactly whenthrows(A)does.throws(Never | Never)should be non-throwing at the call site (multi-leaf extension of SE-0413'sthrows(Never)rule).
That's not a switch-exhaustiveness question β it's "is this function effectively throwing at this call site". For an enum desugar, the call-site try requirement is keyed on the static throws type, not on enum case-payload inhabitance, so throws(EnumOfNever) would still require try (and SE-0413 special-cases exactly throws(Never) to dodge it). Multi-leaf inhabited-subset folding asks the type-checker to generalise that special-case to any leaf-set whose inhabited subset is empty β visible to the type-checker only because the leaf set is in the static type, not behind an enum case constructor. Section 1.2 of my last update lists the five sites that rule has to fire at; the prototype wires all five. So Space-on-an-enum-desugar handles part of the surface (switch arms) but not the throws / try-requirement part.
T | Nominal
whereT` is an archetype
Not supported in v1 β proposal Β§The type restricts each leaf to "a fully concrete type at the point the alternation is written". Generic-typed leaves (T | String inside func foo<T>()) would defer leaf-membership checks to substitution time, which is a different kind of work β leaf-set intersection becomes parametric on T, and the closed-conformer-set reasoning loses the closedness it relies on. Worth flagging as a future direction; I'll add an explicit entry rather than leaving readers to infer it from the silence.
On the "order-free at constraint position" framing. That was an over-claim in an earlier draft and has been retracted β see Β§5.6 of the last update. v1 honours Spelling is identity everywhere a narrowed-Any is bound to a name, including the constraint position. where T == A | B and where T == B | A produce different bindings (different mangled name, different witness identity); a caller passing a value spelled B | A reshapes via explicit as at the call site (free SIL relabel). Two func foo<T == A | B>() and func foo<T == B | A>() declarations were duplicates only under the over-claimed framing; under the corrected framing they are different signatures and there is no canonicalization step. The colon form (where T: A | B) is now parser-rejected (Β§6 of the update); the slot is reserved for the True set-membership future direction, which would introduce order-free leaf-set match β at that point disjunctive requirements at the constraint solver and a canonical leaf-sort become real concerns, but that's the FD's problem to solve, not v1's.
if you have order-independence in this position, why not all positions?
To put it more precisely: spelling-as-identity is strict at every position where a narrowed-Any is bound to a name β type, constraint, function signature, witness β because those are where mangled identity, witness selection, and Codable order get decided. Order-independence is allowed only at value-flow boundaries where the compiler can statically prove two things at the use site: (a) the source's static type is determined (a concrete leaf or a narrowed-Any with a fixed leaf set), and (b) the source's leaf set is a subset of the target's leaf set. Under those two conditions the runtime value is one concrete leaf and there is no spelling left to disagree about β the only check is leaf-set inclusion. The proposal's two propagation rules are exactly this: per-leaf try-propagation at try f() rewrap (proposal Β§"Try-propagation is per-leaf, not per-spelling") and return-position propagation at the return expr boundary (Β§"Return-position is per-leaf, not per-spelling") β so func compose() throws(NetworkError | DecodingError) -> Payload { try fetch() /* fetch throws(DecodingError | NetworkError) */ ; ... } type-checks without a cast even though the inner spelling differs. Cross-spelling at function-value assignment, witness conformance, or Codable encoding does not satisfy condition (a) at the relevant boundary β those are signature-identity / encoder-order questions, not value-flow β so spelling-as-identity bites there.
On mangling + back-deploy. Right that the proposal adds a mangling (the XN operator). The reason this still back-deploys is that the system runtime never has to demangle XN. IRGen routes any type tree containing a NarrowedAnyType through the per-module accessor function unconditionally β see commit b5865b95a76 β so the metadata is built by the user's own module, and the runtime resolves it through the same swift_getExistentialMetadata path it already uses for any P. Binaries compiled with a newer toolchain run on any Swift-supporting OS without an OS upgrade. (The newer toolchain is needed to compile the syntax β that's unavoidable for a language addition β but no minimum-deployment-target bump.) Β§ABI compatibility and Β§Implications on adoption walk through the cases.
Well, I never claimed to know Haskell. (Fair enough!)
@fbartho, @grynspan, @tera β this is the proposal's most contentious axis, and I want to engage it directly rather than handwave.
[@fbartho] You're trying to co-opt punctuation / union-syntax which is commutative in other contexts.
Worth being specific about which "other contexts" | is commutative in. Set theory: yes. Boolean OR: yes. Tagged sum types as in Haskell / Rust / OCaml / F# / Swift's own enum: no β Either Int String β Either String Int, the constructors are positional. The mainstream languages with first-class untagged type-level unions (A | B with no runtime tag wrapper) are TypeScript and Scala 3, and both pay for commutativity by giving up something Swift would otherwise want.
TypeScript erases to JS unions structurally β no nominal identity, the discriminator is user-managed at runtime.
Scala 3 canonicalises A | B β‘ B | A (commutative by spec) and is unboxed at runtime β a value of A | B on the JVM is just an A or a B, no wrapper. Concretely: a given Show[A | B] and a given Show[B | A] resolve to the same instance, because after canonicalisation there is no distinct spelling for them to dispatch on. The Swift analogues that the same canonicalisation would erase: per-spelling Codable try-order on overlapping leaves (the user-controlled lever for "decode this JSON 7 as Int or Double?" disappears), per-spelling witness selection in the prototype's per-leaf dispatch tables, per-spelling mangled name. The proposal's Β§ Comparison with how other languages spell the same idea walks the four nearest neighbours.
The two existing Swift type-level constructors that do carry order are tuples ((A, B) β (B, A)) and function-parameter lists ((A, B) -> R β (B, A) -> R). @tera already pointed this out β both are order-significant for the same reason: the spelling drives observable behaviour (positional access, calling convention). A new closed-conformer-set primitive joining them on the order-significant side is consistent with the existing language; auto-sorting it would make it the odd one out.
[@fbartho]
String | Int == Int | Stringis a statement that must be true for me to accept a proposal.
The concrete things that statement would silently change if we made it true:
-
Codable decoder try-order on overlapping wire shapes.
Int | Doubledecodes a JSON7asInt;Double | Intdecodes the same payload asDouble. Two libraries that picked opposite spellings would silently produce different runtime values from the same JSON. The synth's "try leaves in declaration order until one succeeds" rule needs some tiebreaker when more than one leaf can decode a payload, and declaration order is the only user-controlled lever that expresses it. (Per-leaf protocols where dispatch is by dynamic type βhash(into:),<,description,encode(to:)β are not spelling-dependent; the runtime value is one concrete leaf and dispatches directly to that leaf's witness, which is the same regardless of the static spelling. The decoder is the one in the bunch where order matters.) -
Extension target identity.
extension Int | String { func foo() }andextension String | Int { func bar() }would be distinct extension targets; a user-written conformanceextension Int | String: P { ... }wouldn't appear onString | Intvalues. v1 rejects user extensions on narrowed-Anyto defer the mangler/Sema work, but the v1 commitment to spelling-as-identity is what makes the follow-up able to ship as additive β canonicalisation now would foreclose distinct-target identity later, since you can't un-canonicalise without source-breaking. -
Source-to-symbol one-to-one is a load-bearing invariant for a static language.
Int | Stringmangles asSi_SSXN;String | Intmangles asSS_SiXN. MakingString | Int β‘ Int | Stringforces compile-time canonicalisation β the compiler has to sort and dedupe leaves at parse time so both forms collapse to one symbol. That breaks two static-language properties at once:- Source β symbol. You wrote
(String | Int) -> Voidin your declaration; the compiler emits a symbol referencing(Int | String) -> Void; error messages, stack traces, and.swiftinterfacefiles print whichever form the canonicaliser picked, not the one you wrote. Swift's other list-taking constructs β tuple(A, B), function-parameter list, generic argument listFoo<A, B>β all preserve user spelling all the way through to mangling; an order-independentA | Bwould be the language's lone exception. - Distinct-looking declarations silently coalesce.
func foo(x: String | Int)andfunc foo(x: Int | String)would collide as duplicates under canonicalisation, even though in source they look like overloads. Code review, code search, jump-to-definition, and refactoring tools all rely on "two visibly-different signatures are different signatures"; canonicalisation breaks that, and the compiler is forced into "reject the second" (every author learns the canonical form), "silently merge" (last write wins), or "warn and pick one" β none of which compose well with how Swift APIs evolve across files and modules.
The sort algorithm itself becomes ABI surface (changing it silently re-sorts every deployed binary), and runtime-resolving against the sorted form β Scala 3's unboxing route β trades away per-spelling Codable / extension target / witness identity to dodge the asymmetry. Spelling-as-identity is the choice that keeps
A | Bsource β‘ symbol, in the same camp as every other Swift type-system construct. - Source β symbol. You wrote
-
Static optimization paths depend on the spelling. A type-level ordered
A | Blets the compiler lower it to a stable, predictable layout β the Tagged-union layout future direction replaces the existential metadata pointer with an enum-style tag whose values are declaration-order indices. That unlocks:- Cast-site devirtualization.
if let i = v as? Intonv: Int | Stringcompiles to a constant tag-check, not aswift_dynamicCastmetadata lookup. - Switch lowering.
switch v: A | B | C { case let _ as A: ...; case let _ as B: ...; case let _ as C: ... }lowers to a jump table indexed by tag β same shapeenumswitch already gets today. - Per-witness dispatch on the synthesised conformances (
hash(into:),<,description,encode(to:)) can branch directly on the tag rather than going through dynamic-type metadata lookups. @_specialize(where T == Int | String)and similar programmer-directed specialization hints predictably target the spelling the user wrote.
All of these depend on the leaf-set-to-tag-value mapping being stable across compiler versions β tag 0 is always the first declared leaf, tag 1 always the second. Spelling-as-identity gives that for free. Under canonicalisation, tag values are assigned by the canonical sort algorithm, which means any future change to the algorithm silently re-tags every deployed binary β an ABI break with no recovery path. So unordered doesn't just cost the user one explicit
as; it either forecloses the optimization paths that make narrowed-Anycheap at runtime, or it commits the canonical sort algorithm to be ABI-stable forever (with all the design-evolution costs that implies). - Cast-site devirtualization.
Β§ Spelling is identity lays out all three in the rationale table. The cost the user pays in exchange: one explicit as keyword to reshape, runtime-free SIL relabel (single unchecked_addr_cast).
[@fbartho] 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.
This is the strongest point in the post and I want to address it head-on. The proposal's two propagation rules cover exactly the case where the user can't normalize:
- Per-leaf try-propagation at
try f()rewrap accepts a reverse-spelled inner throws set without a cast β proposal Β§"Try-propagation is per-leaf, not per-spelling". - Return-position propagation does the same at
return exprβ Β§"Return-position is per-leaf, not per-spelling".
So func compose() throws(NetworkError | DecodingError) -> Payload { try fetch() /* fetch throws(DecodingError | NetworkError) */; ... } works without a cast β two libraries that exported opposite spellings of the same throws set compose at the consumer with no normalisation step. The sufficient condition is that the compiler can prove at the use site that (a) the source's static type is determined and (b) the source's leaf set is a subset of the target's leaf set. Under those two conditions the runtime value is one concrete leaf and there's no spelling left to disagree about β only a leaf-set inclusion check.
The same asymmetry also shows up at the user-unpacking boundary: switch arms over a narrowed-Any value and catch arms over a typed-throws set do not depend on source-spelling order β switch e { case let s as String: ...; case let i as Int: ... } is exhaustive regardless of which arm the user wrote first, and catch let e as NetworkError { } catch let e as DecodingError { } works whichever order the leaves were declared in the throws set. TypeScript and Scala 3 β the two mainstream precedents with A | B syntax β are useful empirical evidence here: their unions are commutative at the type level, but the actual code patterns users write to consume a union (typeof / instanceof / in narrowing in TS, match patterns in Scala 3, switch-on-discriminator in both) are order-independent at the user level regardless of whether the underlying type theory is commutative. Commutativity in the type system effectively sits unused at the use site. What signature-ordered identity asks the user to give up is the ability to declare two overloads with reverse-ordered union types, or to silently unify a String | Int declaration in library A with an Int | String declaration in library B β neither of which is a use case anyone has ecosystem complaints about in TS or Scala 3. The use-site ergonomics β narrowing, switch arms, catch arms, try-propagation β are order-free in this proposal too, so Swift users get that for free without paying the per-spelling-identity costs (Codable / extension target / mangling) that the previous section argued for.
So the friction users actually feel under spelling-as-identity is concentrated at signature boundaries β function-value assignment, witness conformance, Codable encoding β where order has observable runtime semantics. The read sites, the destructure sites, the catch sites, and the value-flow boundaries (try-propagation, return-position) are all order-free. In the boundary positions where v1 does require an explicit as, "user can't normalize" maps to "user shouldn't without spelling the consequence", because the wire-format / witness consequence is one the type author needs to make explicitly, not have silently reshaped by the type system.
A future-direction relaxation is also on the roadmap for the binding-level case where the user does want explicit opt-in cross-spelling acceptance: the order-insensitive marker on where clauses, e.g. extension Array where Element ~= Int | String { ... }, would match [Int | String], [String | Int], and any other spelling whose sorted leaves agree β opting that one binding out of spelling-as-identity without weakening the language-wide invariant. Not v1, but the door is open for the explicit-opt-in flavour of order-independence where it actually helps.
[@grynspan] all other languages with sum types of which I'm aware would treat these as equivalent (e.g. by automatically sorting the inner type list at compile time).
Two notes on the framing. First, "sum types" in the standard CS sense (Haskell / Rust / OCaml / F# / Swift's own enum) are not commutative β Either Int String is a distinct type from Either String Int, you destructure on case constructors. The languages that auto-sort are specifically the ones with untagged type-level unions: TypeScript and Scala 3. Second, those two don't auto-sort because of any deep mathematical principle β they auto-sort because the surrounding language semantics don't have anywhere for the order to manifest. TypeScript erases at runtime; Scala 3 unboxes and resolves type-class instances against the canonical type. Swift's surrounding semantics do have places where order manifests: declaration order is observable through Codable, the prototype's per-leaf witness dispatch routes through per-spelling tables, and mangled names embed leaf order. Auto-sorting in Swift would silently change those three things, not just the type identity.
[@tera] FWIW, a tuple could be considered a counterpart product type for which
(A,B)!=(B,A).
Exactly β and thanks for surfacing it. The tuple precedent is the strongest in-language argument I can point at.
I'd rather lose this proposal on this axis than ship a v1 that auto-sorts and silently breaks Codable order / witness selection / mangling. If the path forward is "this whole shape needs a different name and a different surface that doesn't reuse |", that's a real argument and I'd want to hear it explicitly rather than try to bolt commutativity onto the current shape.
@Pippin, @Jon_Shier β replying to both since you're hitting related points.
[@Jon_Shier] a feature like this that doesn't enable exhaustive checking just isn't worth it. If I was okay with that I'd just throw
any Error.
The proposal does enable exhaustive checking β that's the load-bearing piece. A switch over a narrowed-Any value is exhaustive without default:
let e: NetworkError | DecodingError = ...
switch e {
case let n as NetworkError: ...
case let d as DecodingError: ...
} // exhaustive β no `default`, no warning
Same for catch arms when paired with typed throws:
do {
try loadUser() // throws(NetworkError | DecodingError | AuthError)
} catch let e as NetworkError { ... }
catch let e as DecodingError { ... }
catch let e as AuthError { ... }
// exhaustive β no trailing `default` / `catch`
The exhaustiveness checker is Maranget pattern usefulness specialised to a sorted leaf-set β decidable in linear time on the leaf-set size. Adding a leaf to a public API's alternation is a source-breaking change that requires every catch site to be updated, same posture as adding an enum case. That's the property the pitch is built around.
[@Pippin] Comma separated
throwslist doesn't look too unfamiliar from all other declaration lists in the language, whereas a|would be.
Comma-separated throws(A, B) is a viable surface for typed throws specifically, but it solves a strictly smaller problem than the pitch is targeting. The closed-conformer-set primitive A | B shows up in four positions, not one:
- Throws position:
throws(A | B). - Value-level:
let v: Int | String = 7. - Constraint position:
where T == Int | String. - Container element:
[Int | String],Set<Int | String>,extension Array where Element == Int | String.
Comma-syntax handles only #1. To extend to #2-#4 you'd either pick a different syntax for those (two surfaces for one concept) or decide #2-#4 aren't worth supporting β which is the position @Dmitriy_Ignatyev took just below in #29 and is a coherent counter-pitch worth its own thread, not something we can settle in the comma-vs-pipe sub-thread.
The other thing comma-syntax doesn't give you for free is cross-library type identity. throws(A, B) declared in two libraries β does it form a type, or is it parser-level sugar for a hidden tuple? If it's a type, what's its mangled name, and can two libraries' throws(A, B) unify without a runtime canonicalisation pass? If it's not a type, can it appear as Result<T, throws(A, B)> or as a generic argument? The pitch's O(N+M) cross-library composition argument depends on the answer being "yes, same type, mangled identically" β and that's what A | B as a type-level construct gives.
The argument I'd make for | over ,: comma is already the inter-element separator in declaration lists where elements are positional (function parameters, tuple components, generic argument lists). For a closed set of leaves, the spelling already in use across CS literature (and in TypeScript / Scala 3 / Ceylon) is |. Reusing that spelling for the same concept in Swift is the lower-friction choice; the argument for , is mainly aesthetic ("looks like other Swift declaration lists"), and aesthetic alignment with positional lists is the wrong cue for a set-of-leaves construct.
[@Pippin, expanded note] This only errors at non-top-level code.
Right β that's an existing Swift bug/feature (top-level do { try foo() } catch ... has an implicit catch for any error type), not specific to typed throws or to this proposal. The lit test bed for narrowed-Any runs at non-top-level for exactly this reason. Worth filing separately if it's blocking real code; the proposal doesn't try to fix it.
@tikitu β both points are fair, going to engage them seriously.
On the Codable concern:
Imagine encoding a mixed array of Point2D and Point3D values, and getting back only Point2D values having silently dropped the
zcoordinate data.
I want to push back on the framing a bit. The synth is a thin fallback mechanism β try A.init(from:), on failure try B.init(from:), until one succeeds. Each init(from:) is the leaf type's own Decodable conformance; narrowed-Any doesn't override, wrap, or strengthen it. Whether a given payload should succeed against Point2D versus Point3D is the leaf types' own Decodable decision, made before narrowed-Any enters the picture. The synth provides the fallback chain; the leaves provide the per-step success/failure semantics.
What you're seeing in the example is Foundation's default Decodable synthesis being lenient about unknown keys β struct Point2D: Codable { let x, y: Double } accepts a {x, y, z} payload because the synthesised init(from:) ignores unknown keys. That's existing Swift / Foundation behaviour and doesn't depend on narrowed-Any. The same payload would round-trip the same way through a hand-rolled enum-based wrapper today (enum Point { case d2(Point2D); case d3(Point3D) } with try?-based fallback), and through any other try-each-leaf-Decodable chain. Users who want strict-key decoding for the overlap case write a custom init(from:) on the leaf type that rejects unknown keys (a couple of lines using container.allKeys); that lifts the symptom whether the leaf participates in narrowed-Any or not. The fix lives at the leaf decoder, where the responsibility actually is.
That said, I want to use the question to reframe Codable-on-narrowed-Any more honestly, because there's a real point underneath: this is a new capability and users will reasonably ask "what's the contract".
Codable-on-narrowed-Any is a new capability β today's Swift has no synthesised Codable for A | B-shaped types, so users hand-roll wrapper enums. The proposal adds one useful default plus a single general escape hatch:
- Declaration-order untagged (v1 default). Suitable for the common case β
Int | String,URL | Data, error sets carrying different message payload types, and overlapping-shape sets where the leaf decoders are strict enough to disambiguate. Round-trip property: data goes back out the same shape it came in. No envelope, no_kinddiscriminator, no transformation. Declaration order is the user-controlled tiebreaker if more than one leaf can decode a payload. - User-defined override (deferred follow-up, paired with extending a narrowed-
Anydirectly):extension Int | String: Codable { custom encode/decode }. The general escape hatch β tagged envelope, OpenAPI discriminator, peek-then-decode, content-driven dispatch, alternation-level strict-key decoding, whatever. v1 rejects user extensions on narrowed-Anyto defer the mangler/Sema work; the user-override route lands when that follow-up lifts.
So the default isn't forced β the v1 synth is one answer to "what should Codable on A | B do by default", and the user-override follow-up gives every other answer the user might want. The roadmap is "a new capability plus one general escape hatch", not "this one flavour or nothing".
Why untagged is the right default, and worth surfacing the argument for: tagged Codable is inherently encode/decode-asymmetric. Encoding wraps the value in an envelope that wasn't in the original input; decoding then has to recognise that envelope. For consuming third-party JSON producers (most external REST APIs return 7 or "hello" directly, not {"type": ..., "value": ...}), tagged encoding changes the wire format β what came in as 7 goes out as a wrapped envelope, breaking round-trip with the upstream. Untagged preserves the round-trip invariant: data goes back out the same shape it came in. That's load-bearing for interop with non-Swift JSON producers, which is the dominant Codable use case.
So tagged is the wrong default for "consume external JSON" use cases (where the API isn't yours), and the right opt-in for "two-end-controlled internal protocol" use cases. The user-override follow-up gives the second case its surface without making the first case suffer.
Where I'm willing to commit before sending: documenting the responsibility model explicitly in Β§ Issue 5 β the synth is a fallback mechanism, leaf-decoder strictness is the leaf's contract not narrowed-Any's, the user-override follow-up is the escape hatch for any wire-format shape (and for alternation-level decoding when leaf-level strictness isn't enough).
More fundamentally: the proposal's job is to define what the A | B syntax produces under Codable, not to mandate that every Codable case should be untagged. Untagged is what this syntax delivers β developers reach for A | B when untagged is the shape they want, and when they want tagged (or any other shape) they write a custom struct or enum with the wire format their domain calls for, the way they already do today. The reason tagged doesn't belong in the stdlib synthesis isn't that tagged is bad; it's that there's no canonical "right" tagged shape ({type, value} vs {kind, body} vs externally-keyed vs internally-tagged-at-the-leaf vs OpenAPI's discriminator-with-mapping), and any choice the stdlib bakes in will mismatch most users' actual wire-format requirements. The same uncertainty that would make "pick one tagged shape" a multi-year bikeshed in evolution is the reason tagged belongs in user code, not stdlib synthesis. Untagged has a single canonical shape with zero design choices, which is exactly the property that lets it ship as a default at all.
Two clarifications on what's already in the proposal that may be relevant:
- The user-override route (#2 above) is the escape hatch for libraries that want explicit wire-format control. It's listed as a deferred follow-up alongside extending narrowed-
Anydirectly (shared mangler / Sema work). It is not a v1 backstop because v1 rejects user extensions on narrowed-Anyentirely. So in v1 the synthesis is the only path; in the follow-up, the user override fires before synthesis. That's the priority rule documented in the proposal's Β§ Extending a narrowed-Anydirectly. - All-fail decoding propagates the last leaf's underlying error verbatim (committed to the prototype shape in Β§5.7 of my last update). When the synth can't decode anything, the user gets a real
DecodingErrorshaped like Swift's existing single-type decode failures. The Point3D-decoded-as-Point2D shape is a different case β leaf decoders' lenient-by-default behaviour, addressed at the leaf decoder (custom strictinit(from:)) or at the alternation level via the user-override follow-up.
On the LLM-revision trust concern:
the way they're being used makes it very hard to judge the epistemic grounding of the proposal text
To answer the underlying question directly: yes, I've used LLMs heavily β for drafting and summarising the written content, and for the spec-conformant parts like the lit test bed β to keep the proposal moving at a cadence I couldn't manage manually. The person who reads, reviews, and signs off on the final draft is still me. I'll concede that with the volume of material here, and the several rounds where running the prototype against the spec overturned earlier framings, I've missed inconsistencies and stale phrasings β but those are the same kinds of mistakes I'd make typing it all myself, just slower. Without LLMs the proposal would be less prototype-iterated and more brittle to the kind of "stale framing" failure the trust question is about, not less; the time the LLM saves goes directly into the spec-vs-prototype rounds that flush those out.
What the workflow does legitimately leave open is the epistemic-grounding question, and the most useful things I can give you that aren't proposal text are the artifacts those iterations leave behind:
- The lit test bed at
swift/test/NarrowedAny/. Single-file%target-run-simple-swiftruntime checks plus a%target-typecheck-verify-swiftdiagnostics.swiftlocking in the negative-path Sema diagnostics (disjoint cast errors, fix-it text, theextension Int | String { }non-nominal note, etc.). If a proposal sentence says "X is rejected with diagnostic Y" and the lit file doesn't lock that in, the proposal sentence is wrong β that's where I'd want to be checked first. - Each commit on the swift fork has a specific motivation in its message β
git log narrowed-any/phase1-pocwalks the change history. - The prototype itself. The
lib/Sema/CSSimplify.cpp/lib/AST/ConformanceLookup.cpp/lib/SILGen/SILGenType.cpppaths the proposal cites are real code at real SHAs, with the cited methods at the cited signatures.
Your concern as written is too sweeping for me to engage with substantively. I'm prepared to stand behind every section of the proposal and every reply I've sent on this thread β can you back the accusation with comparable specificity?
Since @Pippin in #26 made a related argument from a different angle and the two coordinate.
a general
Int | Stringunion doesn't seem sufficiently justified to me. In many years of development, I've never truly needed something likeInt | Stringβ every such need has been perfectly addressable with generic enums likeEither/OneOf, or with a dedicated enum for a specific API.
Two responses, one general, one specific.
General: SE-0413 explicitly punted on multiple-error throws "until we have a way to spell A | B". A v1 that ships only the typed-throws spelling still has to introduce the type-level construct A | B β there's no shortcut to "throws-only" that avoids the type-system addition, because SE-0413's punt is specifically about the type spelling. Once the type-system construct exists, restricting it syntactically to only the throws position is more compiler work, not less, because every position-check needs an "is this inside a throws clause?" gate that the prototype currently doesn't need.
Specific: the value-level uses that motivate it (in addition to typed throws):
- Codable round-trip across mixed-shape JSON β Itai Ferber's 2018 forum post is the canonical motivation. Today's ecosystem workaround is the wrapper-enum-of-cases pattern, which mangles per-library and forces every consumer through a
.case(_)destructure. - SwiftUI's
_ConditionalContent<_ConditionalContent<A, B>, C>ladder, and thebuildEitherboilerplate that emits it. Replacing it withT | F(proposal Β§ Result builders) retires the wrapper struct and inherits the proposal's depth-1 principle for free. - Container extensions:
extension Array where Element == Int | String { ... }is a method-set scoped to a closed leaf set. Today this is spelled by introducing a marker protocol and conforming each leaf to it (high ceremony, doesn't compose across libraries β each library's marker protocol is a different type). - Per-witness conformance synthesis:
Set<Int | String>,JSONEncoder().encode(v),Comparable.<overInt | Double,\(v)interpolation. All work in the prototype today, all driven by the type-level construct existing at value position. Dropping value-level positions means dropping these.
Each of these is its own use case. The specific question I'd ask back: of these four, is there one where you'd say "no, we don't need that even in 5 years"? If not, the path-of-least-resistance is to ship them together with one type-system addition rather than four staged proposals each adding a syntactic position.
Where I'd agree with you: the value-level uses individually are weaker than typed-throws. Typed-throws is the tip of the spear; the rest are coherent value-axis applications of the same closed-conformer-set primitive. If the forum's read is "typed-throws yes, the rest deserve a separate pitch each", that's a coherent scoping; the cost is repeating the type-system addition four more times, and the typed-throws-only v1 still has to land the type-system construct anyway.
Either / OneOf, or with a dedicated enum for a specific API
Worth flagging the implicit concession in your own framing: you're saying every such need has been "perfectly addressable" with Either / OneOf / dedicated enums, which already says the underlying need is real and recurring β you've reached for a wrapper every time. The question isn't whether the need exists; it's whether A | B is a better surface for it than the workarounds. The proposal isn't asking you to stop using Either, OneOf, or hand-rolled enums β they keep working unchanged. It's offering one more option that addresses the same need with a more general spelling, cross-library type identity, no per-arity rewrite, and synthesised conformance for the six standard-library protocols out of the box. Picking between A | B and a wrapper enum stays a per-API call the developer makes; this proposal just adds the option, with enough advantages to be worth having.
Worth being concrete about why those workarounds don't compose, since that's the gap A | B actually closes. A library exporting Either<NetworkError, DecodingError> is not the same type as another library exporting Either<NetworkError, DecodingError> if either library defined its own Either (each library's Either mangles per-module). Even when both libraries use the standard library's Either, two libraries' Either<E1, E2> and Either<E2, E1> (different leaf order) are distinct generic instantiations, so a consumer composing them needs an explicit conversion. A "dedicated enum for a specific API" is great for that API; it's exactly the per-API wrapper that doesn't compose across APIs.
I think it's time to take a step back.
Replying to posts with multiple walls of LLM-generated or mostly-LLM-generated text over the course of an hour, regardless of the actual content of those posts, doesn't feel like it's in the spirit of engaging in good faith with the discussion. The effort and burden are vastly unbalanced; you're presumably generating these posts with relative ease while expecting other human community members to read the entire content of the posts to address those points.
A bit more bluntly, if the feature you're proposing needs this much text to defend its suitability in the language, it may not be the right fit.
You are exactly the kind of person I was talking about in my reply: someone making an overreaching accusation. Why would you assume that spending several hours preparing and checking multiple replies is something easy for me? Do you have some misunderstanding about how quickly LLM-generated content can be produced? If these replies were entirely generated by an LLM and I posted them without even reading them, they would have gone out immediately after I received each response, wouldn't they?
You may have spent half an hour to several hours reviewing this proposal(if so, I do appreciate that), then spent five minutes replying to this thread. I spent nearly a month preparing this proposal. Now, in order to respond accurately to everyone's questions, I have to verify a range of details, check them carefully, and make sure my replies do not conflict with the proposal itself. And somehow, in your telling, that becomes me using an LLM to do this βeasilyβ?
I read the points that needed replies, and I did the checks and proofreading that needed to be done. It is precisely because this proposal is complex enough that I need enough testing and verification to support it. As I said earlier, the LLM only helped me speed up that process. How did that become, in your view, βit's too complicated, so let's not do itβ?
And to those who see that an LLM was used and then dismiss the actual effort other people put in, I genuinely find that shameful.