swift-primitives/swift-tagged-primitives
Phantom-typed value wrappers for zero-cost type safety — Tagged<Tag, Underlying> gives ecosystem types like Index<Element>, Cardinal, Ordinal, and Hash.Value their type-level identity without runtime cost, including across ~Copyable and ~Escapable underlying values.
Key Features
- Zero-cost phantom discrimination —
Tagged<Tag, Underlying>stores exactly one field; with@inlinable, release-mode codegen is identical to the bareUnderlying(verified inExperiments/tagged-zero-cost-codegen). - Operator non-forwarding is a feature — arithmetic on
Underlyingis never automatically available onTagged, preventingIndex<Graph> + Index<Bit>.Countfrom compiling even though both wrap types with a defined+. Operations are declared per-domain with matchingTagconstraints. - Universal
Tag: ~Copyable & ~Escapable— every extension lifts the tag's copyability and escapability constraints, so phantom-typed indices into~Copyablecontainers (Index<Element>whereElement: ~Copyable) do not lose their operators. ~Copyableand~EscapableUnderlying—Taggedadmits move-only and lifetime-bounded wrapped values; the ecosystem's typed pointers and scoped references (Ownership.Inout,Ownership.Borrow) wrap cleanly. Neither stdlib'sRawRepresentablenorpointfreeco/swift-taggedadmits this; both predate Swift's noncopyable-generics features (SE-0427, SE-0446).Ownership.Borrow.Protocolconformance —Tagged<Tag, Underlying>isOwnership.Borrow.ProtocolwhenUnderlyingis;Tagged.Borrowedresolves toUnderlying.Borrowed. The conformance is supplied byswift-ownership-primitives(the package that declares the protocol).- Unconditional
Carrier.\Protocol\`conformance(ships in this package) — Tagged<Tag, Underlying>is always aCarrier.\Protocol\of its immediateUnderlying, regardless of whatUnderlyingis.Tagged.Underlying == Underlying(the immediate generic parameter, not a recursive cascade). The phantomTagbecomes Carrier'sDomaindiscriminator. External access flows throughtagged.underlying(the Carrier accessor, returns the immediate wrapped value); construction flows throughTagged<Tag, U>(value)(the Carrier init). For nestedTagged<X, Tagged<Y, U>>,.underlyingreturnsTagged<Y, U>` — consumers that need the bottom-most type recurse explicitly.
Quick Start
Domain-identity without a parallel struct
import Tagged_Primitives
public enum User {}
public enum Order {}
extension User { public typealias ID = Tagged<User, UInt64> }
extension Order { public typealias ID = Tagged<Order, UInt64> }
let user: User.ID = 42
let order: Order.ID = 42
// user == order // Compile error: Tagged<User, ...> ≠ Tagged<Order, ...>The hand-rolled equivalent per domain — one struct, one init, one underlying accessor, one conformance stack — multiplied across every ID type in the system. Tagged collapses it to one declaration.
Phantom-typed indices into ~Copyable containers
import Tagged_Primitives
import Ordinal_Primitives
public enum File {}
extension File {
public struct Descriptor: ~Copyable { /* resource handle */ }
}
typealias Index<Element: ~Copyable & ~Escapable> = Tagged<Element, Ordinal>
let fd: Index<File.Descriptor> = 3
let byte: Index<UInt8> = 3
// fd == byte // Compile error: File.Descriptor tag ≠ UInt8 tagTag: ~Copyable & ~Escapable on every extension means the index type works whether the element is Copyable, ~Copyable, Escapable, or ~Escapable.
Functor operations — map and retag
import Tagged_Primitives
let id: User.ID = 42
let asString: Tagged<User, String> = id.map { String($0) } // preserve Tag, transform Underlying
let asOrder: Order.ID = id.retag() // preserve Underlying, change Tag (explicit coercion)retag is a phantom coercion — with @inlinable, the optimizer eliminates the call. It is a meaningful operation for domain-identity wrappers because crossing domains IS the intent. (Contrast: Property<Tag, Base> uses the tag as a verb namespace, not a domain identity — retagging makes no sense there.)
Tagged.map uses typed throws (throws(E) where E: Error); the error type is part of the signature, not erased to any Error:
struct ParseError: Error { let message: String }
func parseUserID(_ raw: String) throws(ParseError) -> User.ID {
guard let n = UInt64(raw) else { throw ParseError(message: "not a number") }
return User.ID(n) // Carrier init — accepts the immediate Underlying
}
let id: Tagged<User, String> = "42"
let parsed: User.ID = try id.map { raw throws(ParseError) in
guard let n = UInt64(raw) else { throw ParseError(message: "not a number") }
return n
}External construction flows through the Carrier.\Protocol\`-derived public init Tagged<Tag, U>(_ underlying:) (when U conforms to Carrier.\Protocol\). Domain types layer custom validated initializers on top; the package-internal init(_unchecked:)` exists only for SLI conformances and per-domain types declared inside this package — external consumers should not reach for it.
Consumers who need a Result-shaped outcome wrap at the call site: Result(catching: { try id.map(transform) }).
Installation
dependencies: [
.package(url: "https://github.com/swift-primitives/swift-tagged-primitives.git", branch: "main")
].target(
name: "App",
dependencies: [
.product(name: "Tagged Primitives", package: "swift-tagged-primitives"),
// Optional — opt into stdlib protocol conformances:
// .product(name: "Tagged Primitives Standard Library Integration", package: "swift-tagged-primitives"),
]
)Requires Swift 6.3.1 and macOS 26 / iOS 26 / tvOS 26 / watchOS 26 / visionOS 26 (or the matching Linux / Windows toolchain).
Architecture
Three library products: Tagged Primitives (the umbrella), Tagged Primitives Standard Library Integration (opt-in stdlib conformances), and Tagged Primitives Test Support (test-only fixtures, re-exports SLI for ergonomic test code).
Main target (Tagged Primitives)
| File | Purpose | |------|---------| | Tagged.swift | The Tagged<Tag: ~Copyable & ~Escapable, Underlying: ~Copyable & ~Escapable> struct, functor operations (map, retag), and conditional conformances (Sendable, Equatable, Hashable, Comparable, Codable, BitwiseCopyable). The body holds storage (package-internal) and init(_unchecked:) (public — direct construction bypassing Carrier's consuming init, for cross-package consumers whose Underlying cannot satisfy that requirement, e.g., Property.View over Ownership.Inout). | | Tagged+CustomStringConvertible.swift | CustomStringConvertible forwarded to the underlying value. | | Tagged+Carrier.Protocol.swift | Unconditional Carrier.\Protocol\` conformance — Tagged.Underlying == Underlying (immediate, no cascade). Tagged is always a Carrier of its immediate wrapped type, regardless of what Underlying is. The phantom Tag becomes the Domain discriminator. Provides the public underlying accessor and init(:)` for external consumers. |
Standard Library Integration target (Tagged Primitives Standard Library Integration)
Opt-in via import Tagged_Primitives_Standard_Library_Integration (which re-exports Tagged_Primitives so consumers don't double-import).
| File | Conformance | |------|-------------| | Tagged+Literals.swift | The 7 stdlib literal protocols (ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByBooleanLiteral, ExpressibleByStringLiteral, ExpressibleByUnicodeScalarLiteral, ExpressibleByExtendedGraphemeClusterLiteral, ExpressibleByStringInterpolation) — bundled because they share @disfavoredOverload discipline as a cohesive opt-in family — plus ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral via a documented unsafeBitCast carve-out. This is the package's only exception to its otherwise-strict memory-safety stance; bounded scope (function-type reinterpretation between variadic and array forms only); marked with the unsafe expression keyword. See the file's MARK block and Research/principled-absence-array-dict-literal.md for provenance and ABI commitment status. | | Tagged+Identifiable.swift | Identifiable (forwards id to underlying.id; carries the documented identity-inversion trade-off). | | Tagged+LosslessStringConvertible.swift | LosslessStringConvertible (init?(:) parses, description from main's CustomStringConvertible; lossy-from-Tagged-perspective trade-off documented). | | Tagged+Sequence.swift | Sequence (forwards makeIterator; wrapper-vs-content conflation trade-off documented). | | Tagged+Collection.swift | Collection (forwards startIndex / endIndex / subscript / index(after:)). |
Deliberate absences
Some SLI conformances are deliberately absent where they would imply Foundation dependencies, invalid semantics, or unsupported forwarding. See Research/sli-deliberate-absences.md for the catalogue (three categories, ten entries, each linking to a research doc + paired experiment).
Dependencies
The single direct dependency, swift-carrier-primitives, provides the Carrier.\Protocol\` capability protocol that Tagged: Carrier.\Protocol\ conforms to (unconditionally — Tagged is a Carrier of its immediate Underlying for any Underlying). Other ecosystem-specific conformances on Tagged (Ordinal.Protocol, Ownership.Borrow.Protocol, etc.) live in the respective protocol / capability packages that import swift-tagged-primitives`.
Stability
swift-tagged-primitives follows SemVer pre-release semantics in 0.x.
| Surface | 0.1.x expectation | |---|---| | Public type names | Stable within 0.1.x | | Documented initializers, functor operations, and conformance set (main + SLI) | Stable within 0.1.x; additive changes (new conformances) may land in patch releases | | Internal storage shapes / unsafeBitCast carve-out scope / fork-heritage choreography | Not part of the source-stability commitment |
Platform Support
| Platform | Status | |----------|--------| | macOS 26 | Full support | | Linux | Full support | | Windows | Full support | | iOS / tvOS / watchOS / visionOS | Supported | | Swift Embedded | Supported |
License
Apache 2.0 (Institute) with MIT attribution to the upstream pointfreeco/swift-tagged (Copyright (c) 2019 Point-Free, Inc.). The combined-license text — Institute Apache 2.0 + the upstream's preserved MIT block — is in LICENSE.md. MIT requires preservation of the original copyright notice in derivative works; the Institute's Apache 2.0 governs new contributions on top of the fork point.
Package Metadata
Repository: swift-primitives/swift-tagged-primitives
Default branch: main
README: README.md