Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Language · Swift 6.3 · Swift 6 language mode · Structured Concurrency

Swift

Value semantics, optionals and structured concurrency.

swiftvalue-typesconcurrency

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You are writing modern Swift for a codebase that compiles clean under Swift 6 language mode with full data-race safety. "Good" here means value-typed, immutable-by-default, concurrency-safe code with no force-unwraps, exhaustive error handling, and names that read as English per the Swift API Design Guidelines.

Stack

  • Swift 6.3 (swift.org toolchain; Xcode 26.6 bundles Swift 6.3.3). Compile in Swift 6 language mode — strict concurrency and data-race safety are on, not opt-in. Don't target the Swift 6.4 / Xcode 27 beta toolchain or its beta-only features (async defer, for-in over noncopyables, anyAppleOS) in code that must build on the released toolchain.
  • Swift Package Manager with swift-tools-version:6.3. Manage deps in Package.swift; commit Package.resolved. Build/test with swift build / swift test.
  • Swift Testing (import Testing, @Test, #expect, #require) — bundled in the toolchain, default for new tests. Keep XCTest only for legacy suites and the UI/performance tests it still owns; both frameworks run side by side in one test target, so migrate file-by-file. Don't mix their APIs inside a single test: no #expect/#require in an XCTestCase, no XCTAssert* in a @Test.
  • Approachable Concurrency (Swift 6.2+): nonisolated(nonsending) async by default, main-actor default isolation for app/UI targets, isolated-conformance inference. Enable via the flags below.
  • swift-format (official, in the toolchain: swift format) for formatting + lint. Optionally SwiftLint for extra diagnostics (complexity, file length) it uniquely catches.
  • Logging: swift-log Logger for portable/server code; os.Logger (OSLog) on Apple platforms. Never print in library or production code.
  • Time/async: Duration, ContinuousClock, Task.sleep(for:). Containers: InlineArray / Span (Swift 6.2) for fixed-size, hot-path buffers; swift-collections (OrderedDictionary, Deque, OrderedSet) and swift-algorithms (chunked, windows, uniqued) instead of hand-rolling.

Project conventions

  • Layout: Sources/<Target>/, Tests/<Target>Tests/, one Package.swift per package. One primary type per file; filename matches the type (OrderService.swift).
  • Naming (API Design Guidelines): UpperCamelCase types/protocols, lowerCamelCase members. Methods read as phrases at the call site: mutating verb sort(), non-mutating -ed/-ing noun sorted(). Booleans read as assertions: isEmpty, hasSuffix. Protocols describing capability end in -able/-ible (Equatable); protocols describing a thing are nouns (Collection). No Hungarian/_-prefixed public names, no get prefixes.
  • Imports: import the smallest module. Prefer import struct Foundation.Data style for one symbol in library targets to keep boundaries tight. No wildcard re-exports unless building an umbrella module.
  • Access control: default to internal; mark private/fileprivate aggressively; only public/package what the module contract needs. Libraries: use package (Swift 5.9+) for cross-target-internal API instead of over-exposing public.
  • Formatting: swift format --in-place --recursive Sources Tests; enforce in CI with swift format lint --strict --recursive Sources Tests. Commit a .swift-format JSON config (2-space indent, line length 120). No hand-formatting fights with the tool.
  • Concurrency + safety flags in every target's swiftSettings:
swiftSettings: [
    .swiftLanguageMode(.v6),
    .defaultIsolation(MainActor.self),                        // SE-0466: app/UI/executable targets
    .strictMemorySafety(),                                    // SE-0458: makes `unsafe` uses explicit
    .enableUpcomingFeature("NonisolatedNonsendingByDefault"), // SE-0461
    .enableUpcomingFeature("InferIsolatedConformances"),      // SE-0470
]

Omit .defaultIsolation(MainActor.self) for concurrency-heavy library targets that should stay non-isolated. .strictMemorySafety() is the dedicated SwiftSetting (equivalently the -strict-memory-safety flag) — it is not an .enableUpcomingFeature flag; StrictMemorySafety is only the diagnostic-group name.

Value types first

  • Model data with struct and enum. Reach for class only when you genuinely need reference identity, inheritance, deinit, or Objective-C interop — say why in a comment. final class when a class is unavoidable and not designed for subclassing.
  • Reference-observing UI/domain models: use the Observation framework @Observable (a macro on a final class), not ObservableObject/@Published.
  • Default properties to let. Use var only for state that provably changes. Local bindings: let unless mutated. This is a data-race and correctness lever, not a style preference.
  • Let the compiler synthesize Equatable, Hashable, Codable, and Sendable conformances for value types; only hand-write them when the derived behavior is wrong. Add CustomStringConvertible for meaningful logging, not debugPrint hacks.
  • Value semantics + large payloads: rely on copy-on-write (Array, Dictionary, String already do it). Hand-roll COW with an internal storage class only when profiling shows copies hurt.
  • Make small perf-critical or ownership-sensitive types ~Copyable and use borrowing/consuming parameter ownership to eliminate copies; expose access through borrowing accessors rather than copying the value out.

Optionals

  • Never force-unwrap ! or force-cast as! or try! in shipping code. The only tolerated ! is a documented, locally-provable invariant (URL(string: "https://apple.com")! for a compile-time-constant literal) with a // safe: literal comment — prefer designing it away.
  • Unwrap with if let x/guard let x (shorthand, no = x), switch, or ??. Use guard let … else { return/throw } for early exit so the happy path stays unindented.
  • Optional chaining a?.b?.c over nested if let pyramids. map/flatMap on optionals for transforms.
  • Don't use ?? fatalError() tricks; if a nil is truly impossible, model it out of the type. Avoid Optional<Bool> tri-state flags — use an enum.

Enums and pattern matching

  • Model finite states and heterogeneous payloads with enum + associated values (case success(Data), case failure(APIError)); avoid struct-of-optionals "one of these is set" shapes.
  • switch over enums without a default clause so the compiler forces you to handle new cases. Use default only for genuinely open matching.
  • Use if case/guard case for single-case extraction, for case .some(let x) to filter, where clauses in cases. Bind once: case let .point(x, y).
  • Mark enums you don't want exhaustively matched across module boundaries as @frozen only when you commit to never adding cases (public ABI stability); default to non-frozen.

Errors

  • Throw typed, meaningful errors: an enum ServiceError: Error with associated context, not NSError or stringly-typed errors. Conform to LocalizedError only where a user-facing message is needed.
  • Use typed throws (Swift 6.0+) where the error set is closed and callers benefit: func load() throws(ServiceError) -> Model. Keep untyped throws for boundaries that propagate heterogeneous errors; don't force typed throws where it leaks implementation detail.
  • Result<Success, Failure> for stored/deferred outcomes and completion-style boundaries you can't yet make async. Bridge with Result { try … } and .get(). Prefer async/throws over Result for new call flows.
  • do { … } catch let e as ServiceError { … } catch { … }. Pattern-match error cases in catch. Never catch {} silently — log or rethrow. Use defer for synchronous cleanup that must run on every exit path.
  • Reserve fatalError/precondition/assert for programmer errors and unreachable invariants, never for recoverable runtime failures.

Protocol-oriented design and generics

  • Design to protocols + protocol extensions for default implementations; compose small protocols over deep class hierarchies. Add capability via extension conformances, retroactively with @retroactive when conforming a foreign type to a foreign protocol.
  • Prefer generics <T: Codable> and opaque results some View/some Collection over boxed existentials. Use any Protocol only when you truly need heterogeneous storage or dynamic dispatch, and write any explicitly — existentials require the any spelling in the Swift 6 language mode (introduced by SE-0335 in Swift 5.6).
  • Constrain with where clauses and primary associated types (some Collection<Int>, any Sequence<Element>). Use parameter packs (each T) for variadic-generic APIs instead of arity-N overloads. Avoid protocols-as-types when a generic constraint gives static dispatch and specialization.
  • Don't reach for AnyView/type erasure to dodge a generic signature; erase only at real boundaries. Reach for perf attributes — @inlinable/@usableFromInline, and Swift 6.3's @specialize/@inline(always)/@export — only after profiling proves the win, and keep them out of API you don't want frozen.
  • Expose Swift functions/enums to C with the @c attribute (Swift 6.3) rather than manual @_cdecl shims when you need a C-callable boundary.

Concurrency (structured, data-race-safe)

  • All new async code uses async/await. No completion-handler pyramids, no DispatchQueue.async nesting for new code — bridge legacy callbacks once with withCheckedThrowingContinuation and resume exactly once. Turn callback-based streams into AsyncStream/AsyncThrowingStream.
  • Protect shared mutable state with actor (or @MainActor for UI/main-thread state). Don't guard state with locks/DispatchQueue for new code; the actor is the unit of isolation. Keep actor methods short; hop off with nonisolated for pure work. Beware actor reentrancy — state can change across an await, so re-check invariants after suspension.
  • Everything crossing an isolation boundary must be Sendable. Make value types Sendable (often automatic); mark thread-safe reference types @unchecked Sendable only with a comment explaining the manual synchronization. Prefer Sendable closures typed explicitly.
  • Structure concurrency: async let for a fixed set of parallel children; withTaskGroup/withThrowingTaskGroup for dynamic fan-out; for try await over AsyncSequence. Children are awaited/cancelled with the parent — no orphaned Task {} that outlives its scope.
  • Honor cancellation: check Task.isCancelled / try Task.checkCancellation() in loops; propagate it, don't swallow CancellationError. Prefer withTaskCancellationHandler to tear down non-Swift resources.
  • Under Approachable Concurrency, an async function is nonisolated(nonsending) by default — it runs on the caller's executor and doesn't hop unless you opt into @concurrent. Use @concurrent deliberately to move work off the current actor.
  • Pass context implicitly with task-local values (@TaskLocal) instead of threading it through every signature; define custom global actors sparingly (@MainActor covers most UI cases).
  • Never block a thread: no DispatchSemaphore.wait(), Thread.sleep, or synchronous .wait() inside async code. Sleep with try await Task.sleep(for: .seconds(1)) using a Clock.
  • MainActor.assumeIsolated only when you can prove main-thread execution; otherwise await MainActor.run { … }.
  • Control where work runs with executors, not raw threads: pin CPU-bound task groups to a TaskExecutor (withTaskExecutorPreference) rather than spawning Threads or hopping through DispatchQueues.

Testing

  • Write new tests with Swift Testing. Free functions or methods in a @Suite struct annotated @Test. Assert with #expect(x == y) (soft, keeps going) and unwrap/precondition with try #require(optional) (hard, stops the test) — these two replace all XCTAssert*.
  • Errors: #expect(throws: ServiceError.notFound) { try sut.load() } or #expect(throws: (any Error).self). Async callbacks: await confirmation { confirm in … }. Known failures: wrap in withKnownIssue { … }. Fatal-path code: cover with exit tests (await #expect(processExitsWith: .failure) { … }).
  • Parameterize instead of copy-pasting: @Test(arguments: [1, 2, 3]) or zipped/cross-product argument collections; each case reports independently.
  • Organize with @Suite("Name"), nest suites, use .tags(.integration), .timeLimit(.minutes(1)), and .serialized (for order-dependent suites; tests run in parallel by default). Gate with .enabled(if:) / .disabled("reason"). Use init/deinit of the suite struct for setup/teardown — no setUp/tearDown boilerplate.
  • Test behavior and edge cases (nil, empty, boundary, cancellation, error paths), not getters. Keep tests deterministic: inject clocks (ContinuousClock/a test clock), inject dependencies via protocols, no real network or sleep. Name tests as sentences describing the expectation.

Security

  • Validate and parse all external input at the boundary into typed models via Codable with explicit CodingKeys; never trust field presence. Decode with JSONDecoder and handle DecodingError — don't try!.
  • No secrets in source or logs. Load from environment/secure store; on Apple platforms use Keychain, not UserDefaults. Redact tokens/PII before logging; swift-log metadata should not carry secrets.
  • Keep .strictMemorySafety() on and avoid Unsafe*Pointer, unsafeBitCast, unsafeDowncast. If unavoidable, isolate in a small reviewed function, mark the call unsafe, and document the invariant.
  • For TLS/HTTP use URLSession (or a vetted client); don't disable certificate validation. Build shell/DB/URL strings from typed components, never string interpolation of untrusted input.
  • Use CryptoKit/swift-crypto for hashing/crypto — never roll your own. Compare secrets in constant time. Prefer Data over String for key material and zero it when done.

Standard library, strings, and Foundation

  • Strings are collections of Character (grapheme clusters), not bytes. Never index with an Int — use String.Index, firstIndex(of:), prefix/suffix, or the .unicodeScalars/.utf8 views chosen deliberately. Compare user-facing text with localizedStandardCompare/caseInsensitiveCompare, not ad-hoc lowercased().
  • Wrap primitive IDs in a typed struct OrderID: Hashable, Codable, RawRepresentable (or a phantom-typed ID<Order>) rather than passing bare String/UUID, so the compiler stops you mixing an order id with a user id.
  • Format with FormatStyle: value.formatted(.number), price.formatted(.currency(code: "EUR")), date.formatted(.dateTime.year().month().day()), duration.formatted(.units()). Reach for NumberFormatter/DateFormatter only for legacy interop, and cache them if you do.
  • Money and time: use Decimal (or minor-unit integers) for currency and Duration/Date for time — never Double. Choose an explicit JSONDecoder.dateDecodingStrategy (usually .iso8601); never parse dates by slicing strings.
  • Transform with map/compactMap/filter/reduce(into:)/first(where:); use .lazy to avoid materializing intermediates on large sequences, and swift-algorithms (chunked, windows, uniqued) over hand-rolled loops.
  • Integer overflow traps by default — that's correct; use &+/addingReportingOverflow only where wrapping is intentional and documented. Pick fixed-width types (Int32, UInt8) deliberately at serialization boundaries.

Modeling and API design

  • Push validation into initializers: a failable init? or throws init that rejects bad input, so once a value exists it is guaranteed valid ("parse, don't validate").
  • Prefer many small, single-responsibility types with clear ownership over one god-struct of loosely related fields. Give each a focused, testable surface.
  • Default function parameters over overload families; label arguments so the call site reads as a sentence. Avoid boolean parameters that are opaque at the call site — use an enum (show(.animated) over show(true)).
  • Document public/package API with /// doc comments (summary, - Parameters:, - Returns:, - Throws:); keep the surface minimal and additive to protect ABI/source stability.

Do

  • Return some/opaque or concrete types; keep public API minimal and documented with ///.
  • Prefer immutable, Sendable value types passed across concurrency boundaries.
  • Use guard for preconditions and early return to keep the happy path flat.
  • Exhaustively switch enums; let the compiler flag unhandled new cases.
  • Model impossible states out of existence with enums and non-optional types.
  • Inject dependencies through protocols for testability; construct concrete types at the composition root.
  • Use Duration/Clock APIs and structured TaskGroups for parallelism.
  • Parse-don't-validate at the boundary: make invalid values unrepresentable via failable/throwing initializers.
  • Run swift build, swift format lint --strict, and swift test before proposing a change.

Avoid

  • Force-unwrap !, force-cast as!, try! — replace with guard let/if let/as?/typed throws. (Only exception: a commented, literal-backed invariant.)
  • class by default — use struct/enum; ObservableObject/@Published for models — use @Observable.
  • Completion-handler nesting and DispatchQueue/OperationQueue orchestration for new code — use async/await + actors. Bridge legacy with withCheckedContinuation.
  • Locks/semaphores for shared state — use actor; blocking Thread.sleep/semaphore.wait() in async code — use Task.sleep.
  • Detached Task {} fire-and-forget with no lifecycle owner; ignoring Task.isCancelled.
  • @unchecked Sendable without a synchronization comment; NSError/stringly-typed errors — use typed Error enums.
  • default: on domain enums that swallows future cases; struct-of-optionals modeling of mutually exclusive states.
  • print for logging; any Protocol where a generic constraint would give static dispatch and specialization.
  • Indexing String by Int, comparing user text via lowercased(), and Double for money — use String.Index, localizedStandardCompare, and Decimal.
  • Beta-toolchain-only syntax (async defer, for-in over noncopyables, anyAppleOS) in code that must build on the released Swift 6.3 toolchain.

When you code

  • Make small, focused diffs. Touch one concern per change; don't opportunistically reformat unrelated code (let swift format own formatting).
  • After editing, run swift build (must be warning-clean under Swift 6 mode — treat concurrency warnings as errors), then swift format lint --strict, then swift test. Report failures; don't paper over a data-race warning with @unchecked Sendable or nonisolated(unsafe).
  • When adding a dependency, pin it in Package.swift, update Package.resolved, and justify it over the stdlib/Foundation.
  • Ask before: changing a target's default isolation, introducing @unchecked Sendable/unsafe, breaking public/package API, adding a new dependency, or switching a value type to a reference type. Otherwise proceed and note the decision.
  • If a requested change would force a force-unwrap, a silent catch {}, or a blocking wait in async code, propose the safe alternative instead.
  • Target the released toolchain: build against Swift 6.3 (swift --version), and don't adopt a Swift 6.4 / Xcode 27 beta feature until it ships in a stable release.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Swift 6.3 · Swift 6 language mode · Structured Concurrency.

Back to top ↑