home / skills / charleswiltgen / axiom / axiom-ownership-conventions

This skill helps you optimize large value type performance by applying explicit borrowing and consuming ownership to reduce copies and ARC traffic.

npx playbooks add skill charleswiltgen/axiom --skill axiom-ownership-conventions

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.9 KB
---
name: axiom-ownership-conventions
description: Use when optimizing large value type performance, working with noncopyable types, or reducing ARC traffic. Covers borrowing, consuming, inout modifiers, consume operator, ~Copyable types.
license: MIT
metadata:
  version: "1.0.0"
---

# borrowing & 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 |
| `borrowing` | Caller keeps | Explicit `copy` only | Read-only, large types |
| `consuming` | Caller transfers | None needed | Final use, factories |
| `inout` | Caller keeps, mutable | None | Modify in place |

## Default Behavior by Context

| Context | Default | Reason |
|---------|---------|--------|
| Function parameters | `borrowing` | Most params are read-only |
| Initializer parameters | `consuming` | Usually stored in properties |
| Property setters | `consuming` | Value is stored |
| Method `self` | `borrowing` | Methods read self |

## Patterns

### Pattern 1: Read-Only Large Struct

```swift
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

```swift
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:

```swift
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:

```swift
let data = loadLargeData()
process(consume data)
// data is now invalid — compiler prevents use
```

### Pattern 5: Noncopyable Type

For `~Copyable` types, ownership modifiers are **required**:

```swift
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

```swift
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

```swift
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

```swift
// ❌ 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

```swift
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

```swift
// ❌ 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
}
```

## 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)

## Decision Tree

```
Need explicit ownership?
├─ Working with ~Copyable type?
│  └─ Yes → Required (borrowing/consuming)
├─ 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

**WWDC**: 2024-10170

**Skills**: axiom-swift-performance, axiom-swift-concurrency

Overview

This skill documents clear ownership conventions for Swift-style parameter modifiers (borrowing, consuming, inout, consume operator) to optimize large value types, support noncopyable (~Copyable) types, and reduce ARC traffic. It bundles practical patterns, default behaviors, and a decision flow to help you choose when to borrow, consume, or let the compiler decide. Use it when performance or ownership safety matters in modern xOS codebases.

How this skill works

The guidance inspects call-site intent and type characteristics to recommend explicit ownership modifiers: borrowing for read-only access without implicit copies, consuming for transferring ownership (no copies), and inout for in-place mutation. It highlights when copy operations must be explicit and when ownership modifiers are required for ~Copyable types. It also points out ARC-reduction opportunities for reference types.

When to use it

  • Passing large value types read-only to avoid implicit copies
  • Working with noncopyable (~Copyable) resources where modifiers are required
  • Reducing measurable ARC retain/release traffic in hot code paths
  • Factory or builder APIs that should take ownership and invalidate the caller’s object
  • Performance-critical loops where profiling shows copy or ARC overhead

Best practices

  • Default to compiler-chosen behavior unless profiling or semantics demand change
  • Use borrowing for read-only large structs and classes to avoid copies and ARC
  • Use consuming for final-use APIs and builders to express transfer of ownership
  • Call out explicit copy operations (copy value) when storing from a borrowing parameter
  • Avoid over-optimizing trivially copyable small types like Int or Bool

Example use cases

  • Process a megabyte-sized buffer without copying using borrowing parameters
  • Implement a builder whose build() consumes the builder so it can’t be reused
  • Expose a file handle type marked ~Copyable with borrowing read and consuming close
  • Reduce ARC traffic when inspecting expensive reference objects by borrowing them
  • Write a Transaction.commit() that consumes self to guarantee single use after commit

FAQ

When should I prefer borrowing over consuming?

Prefer borrowing when you only need read-only access and want to avoid copies or ARC operations; use consuming when the callee must take ownership or the value is no longer needed by the caller.

Are these modifiers required for ~Copyable types?

Yes. For noncopyable (~Copyable) types, ownership modifiers are mandatory to express safe transfer or temporary access.

Will I lose ABI stability by adding ownership modifiers?

Modifiers can affect ABI; prefer them for internal performance-critical APIs and rely on profiling before changing widely used public interfaces.