Skillshub axiom-ownership-conventions
Use when optimizing large value type performance, working with noncopyable types, reducing ARC traffic, or using InlineArray/Span for zero-copy memory access. Covers borrowing, consuming, inout modifiers, consume operator, ~Copyable types, InlineArray, Span, value generics.
git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/CharlesWiltgen/Axiom/axiom-ownership-conventions" ~/.claude/skills/comeonoliver-skillshub-axiom-ownership-conventions && rm -rf "$T"
skills/CharlesWiltgen/Axiom/axiom-ownership-conventions/SKILL.mdborrowing & consuming — Parameter Ownership
Explicit ownership modifiers for performance optimization and noncopyable type support.
When to Use
✅ Use when:
- Large value types being passed read-only (avoid copies)
- Working with noncopyable types (
)~Copyable - Reducing ARC retain/release traffic
- Factory methods that consume builder objects
- Performance-critical code where copies show in profiling
❌ Don't use when:
- Simple types (Int, Bool, small structs)
- Compiler optimization is sufficient (most cases)
- Readability matters more than micro-optimization
- You're not certain about the performance impact
Quick Reference
| Modifier | Ownership | Copies | Use Case |
|---|---|---|---|
| (default) | Compiler chooses | Implicit | Most cases |
| Caller keeps | Explicit only | Read-only, large types |
| Caller transfers | None needed | Final use, factories |
| Caller keeps, mutable | None | Modify in place |
Default Behavior by Context
| Context | Default | Reason |
|---|---|---|
| Function parameters | | Most params are read-only |
| Initializer parameters | | Usually stored in properties |
| Property setters | | Value is stored |
Method | | Methods read self |
Patterns
Pattern 1: Read-Only Large Struct
struct LargeBuffer { var data: [UInt8] // Could be megabytes } // ❌ Default may copy func process(_ buffer: LargeBuffer) -> Int { buffer.data.count } // ✅ Explicit borrow — no copy func process(_ buffer: borrowing LargeBuffer) -> Int { buffer.data.count }
Pattern 2: Consuming Factory
struct Builder { var config: Configuration // Consumes self — builder invalid after call consuming func build() -> Product { Product(config: config) } } let builder = Builder(config: .default) let product = builder.build() // builder is now invalid — compiler error if used
Pattern 3: Explicit Copy in Borrowing
With
borrowing, copies must be explicit:
func store(_ value: borrowing LargeValue) { // ❌ Error: Cannot implicitly copy borrowing parameter self.cached = value // ✅ Explicit copy self.cached = copy value }
Pattern 4: Consume Operator
Transfer ownership explicitly:
let data = loadLargeData() process(consume data) // data is now invalid — compiler prevents use
Pattern 5: Noncopyable Type
For
~Copyable types, ownership modifiers are required:
struct FileHandle: ~Copyable { private let fd: Int32 init(path: String) throws { fd = open(path, O_RDONLY) guard fd >= 0 else { throw POSIXError.errno } } borrowing func read(count: Int) -> Data { // Read without consuming handle var buffer = [UInt8](repeating: 0, count: count) _ = Darwin.read(fd, &buffer, count) return Data(buffer) } consuming func close() { Darwin.close(fd) // Handle consumed — can't use after close() } deinit { Darwin.close(fd) } } // Usage let file = try FileHandle(path: "/tmp/data.txt") let data = file.read(count: 1024) // borrowing file.close() // consuming — file invalidated
Pattern 6: Reducing ARC Traffic
class ExpensiveObject { /* ... */ } // ❌ Default: May retain/release func inspect(_ obj: ExpensiveObject) -> String { obj.description } // ✅ Borrowing: No ARC traffic func inspect(_ obj: borrowing ExpensiveObject) -> String { obj.description }
Pattern 7: Consuming Method on Self
struct Transaction { var amount: Decimal var recipient: String // After commit, transaction is consumed consuming func commit() async throws { try await sendToServer(self) // self consumed — can't modify or reuse } }
Common Mistakes
Mistake 1: Over-Optimizing Small Types
// ❌ Unnecessary — Int is trivially copyable func add(_ a: borrowing Int, _ b: borrowing Int) -> Int { a + b } // ✅ Let compiler optimize func add(_ a: Int, _ b: Int) -> Int { a + b }
Mistake 2: Forgetting Explicit Copy
func cache(_ value: borrowing LargeValue) { // ❌ Compile error self.values.append(value) // ✅ Explicit copy required self.values.append(copy value) }
Mistake 3: Consuming When Borrowing Suffices
// ❌ Consumes unnecessarily — caller loses access func validate(_ data: consuming Data) -> Bool { data.count > 0 } // ✅ Borrow for read-only func validate(_ data: borrowing Data) -> Bool { data.count > 0 }
~Copyable Limitations
Know the constraints before adopting ~Copyable:
| Limitation | Impact | Workaround |
|---|---|---|
Can't store in , , | Collections require | Use wrapper or manage manually |
| Can't use with most generics | implicitly means | Use (requires library support) |
| Protocol conformance restricted | Most protocols require | Use protocol definitions |
| Can't capture in closures by default | Closures copy captured values | Use closure parameters |
| No existential support | doesn't work | Use generics instead |
Common compiler errors when adopting ownership modifiers:
// Error: "Cannot implicitly copy a borrowing parameter" // Fix: Add explicit `copy` or change to consuming func store(_ v: borrowing LargeValue) { self.cached = copy v // ✅ Explicit copy } // Error: "Noncopyable type cannot be used with generic" // Fix: Constrain generic to ~Copyable func use<T: ~Copyable>(_ value: borrowing T) { } // ✅ // Error: "Cannot consume a borrowing parameter" // Fix: Change to consuming if you need ownership transfer func takeOwnership(_ v: consuming FileHandle) { } // ✅ // Error: "Missing 'consuming' or 'borrowing' modifier" // Fix: ~Copyable types require explicit ownership on all methods struct Token: ~Copyable { borrowing func peek() -> String { ... } // ✅ Explicit consuming func redeem() { ... } // ✅ Explicit }
When NOT to use ~Copyable:
- If you need collection storage (arrays, dictionaries)
- If you need to work with existing generic APIs
- If the type needs broad protocol conformance
- Prefer
on regular types as a lighter alternative for "use once" semanticsconsuming func
Performance Considerations
When Ownership Modifiers Help
- Large structs (arrays, dictionaries, custom value types)
- High-frequency function calls in tight loops
- Reference types where ARC traffic is measurable
- Noncopyable types (required, not optional)
When to Skip
- Default behavior is almost always optimal
- Small value types (primitives, small structs)
- Code where profiling shows no benefit
- API stability concerns (modifiers affect ABI)
InlineArray
Fixed-size, stack-allocated array using value generics. No heap allocation, no reference counting, no copy-on-write.
Declaration
@frozen struct InlineArray<let count: Int, Element> where Element: ~Copyable
The
let count: Int is a value generic — the size is part of the type, checked at compile time. InlineArray<3, Int> and InlineArray<4, Int> are different types.
When to Use InlineArray
| Use InlineArray | Use Array |
|---|---|
| Size known at compile time | Size changes at runtime |
| Hot path needing zero heap allocation | Copy-on-write sharing is beneficial |
| Embedded in other value types | Frequently copied between variables |
| Performance-critical inner loops | General-purpose collection needs |
Canonical Example
// Fixed-size, inline storage — no heap allocation var matrix: InlineArray<9, Float> = [1, 0, 0, 0, 1, 0, 0, 0, 1] matrix[4] = 2.0 // Type inference works for count, element, or both let rgb: InlineArray = [0.2, 0.4, 0.8] // InlineArray<3, Double> // Eager copy on assignment (no COW) var copy = matrix copy[0] = 99 // matrix[0] still 1
Memory Layout
Elements are stored contiguously with no overhead:
MemoryLayout<InlineArray<3, UInt16>>.size // 6 (2 bytes × 3) MemoryLayout<InlineArray<3, UInt16>>.alignment // 2 (same as UInt16)
~Copyable Integration
InlineArray supports noncopyable elements — enables fixed-size collections of unique resources:
struct Sensor: ~Copyable { var id: Int } var sensors: InlineArray<4, Sensor> = ... // Valid: ~Copyable elements allowed
Span — Safe Contiguous Memory Access
Span replaces unsafe pointers with compile-time-enforced safe memory views. Zero runtime overhead.
The Span Family
| Type | Access | Use Case |
|---|---|---|
| Read-only elements | Safe iteration, passing to algorithms |
| Read-write elements | In-place mutation without copies |
| Read-only bytes | Binary parsing, protocol decoding |
| Read-write bytes | Binary serialization |
| Write-only | Initializing new collection storage |
| Read-only UTF-8 | Safe Unicode processing |
Accessing Spans
Containers with contiguous storage expose
.span and .mutableSpan:
let array = [1, 2, 3, 4] let span = array.span // Span<Int> var mutable = [10, 20, 30] var ms = mutable.mutableSpan // MutableSpan<Int> ms[0] = 99
Lifetime Safety — Compile-Time Enforcement
Spans are non-escapable — the compiler guarantees they cannot outlive the container they borrow from:
// ❌ Cannot return span that depends on local variable func getSpan() -> Span<UInt8> { let array: [UInt8] = Array(repeating: 0, count: 128) return array.span // Compile error } // ❌ Cannot capture span in closure let span = array.span let closure = { span.count } // Compile error // ❌ Cannot access span after mutating original var array = [1, 2, 3] let span = array.span array.append(4) // span[0] // Compile error: container was modified
These constraints prevent use-after-free, dangling pointers, and overlapping mutation at compile time with zero runtime cost.
Span vs Unsafe Pointers
| Span | UnsafeBufferPointer | |
|---|---|---|
| Memory safety | Compile-time enforced | Manual, error-prone |
| Lifetime tracking | Automatic, non-escapable | None — dangling pointers possible |
| Runtime overhead | Zero | Zero |
| Use-after-free | Impossible | Common source of crashes |
Canonical Example — Binary Parsing
func parseHeader(_ data: borrowing [UInt8]) -> Header { var raw = data.span.rawSpan // RawSpan over the array's bytes let magic = raw.unsafeLoadUnaligned(as: UInt32.self) raw = raw.extracting(droppingFirst: 4) let version = raw.unsafeLoadUnaligned(as: UInt16.self) return Header(magic: magic, version: version) }
When to Use Span
- Replace
— same performance, compile-time safetyUnsafeBufferPointer - Performance-critical algorithms — direct memory access without copying
- Binary parsing/serialization —
for byte-level accessRawSpan - Passing data between functions — borrow the container, pass the span
- UTF-8 processing —
for safe string byte accessUTF8Span
Value Generics
Value generics allow integer values as generic parameters, making sizes part of the type system:
// `let count: Int` is a value generic parameter struct InlineArray<let count: Int, Element> { ... } // Different counts = different types let a: InlineArray<3, Int> = [1, 2, 3] let b: InlineArray<4, Int> = [1, 2, 3, 4] // a = b // Compile error: different types
Currently limited to
Int parameters. Enables stack-allocated, fixed-size abstractions where the compiler verifies size compatibility at compile time.
Decision Tree
Need explicit ownership? ├─ Working with ~Copyable type? │ └─ Yes → Required (borrowing/consuming) ├─ Fixed-size collection, no heap allocation? │ └─ Yes → InlineArray<let count, Element> ├─ Need safe pointer-like access to contiguous memory? │ ├─ Read-only? → Span<Element> │ ├─ Mutable? → MutableSpan<Element> │ └─ Raw bytes? → RawSpan / MutableRawSpan ├─ Large value type passed frequently? │ ├─ Read-only? → borrowing │ └─ Final use? → consuming ├─ ARC traffic visible in profiler? │ ├─ Read-only? → borrowing │ └─ Transferring ownership? → consuming └─ Otherwise → Let compiler choose
Resources
Swift Evolution: SE-0377, SE-0453 (Span), SE-0451 (InlineArray), SE-0452 (value generics)
WWDC: 2024-10170, 2025-245, 2025-312
Docs: /swift/inlinearray, /swift/span
Skills: axiom-swift-performance, axiom-swift-concurrency