Add scan flow MVP and local Axiom skill workspace

This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
Matthias
2026-04-19 21:11:32 +02:00
parent 577214d474
commit a60a76b797
679 changed files with 138964 additions and 73 deletions

View File

@@ -0,0 +1,631 @@
---
name: axiom-lldb
description: Use when ANY runtime debugging is needed — setting breakpoints, inspecting variables, evaluating expressions, analyzing threads, or reproducing crashes interactively with LLDB
license: MIT
---
# LLDB Debugging
Interactive debugging with LLDB. The debugger freezes time so you can interrogate your running app — inspect variables, evaluate expressions, navigate threads, and understand exactly why something went wrong.
**Core insight:** "LLDB is useless" really means "I don't know which command to use for Swift types." This is a knowledge-gap problem, not a tool problem.
## Red Flags — Check This Skill When
| Symptom | This Skill Applies |
|---------|-------------------|
| Need to inspect a variable at runtime | Yes — breakpoint + inspect |
| Crash you can reproduce locally | Yes — breakpoint before crash site |
| Wrong value at runtime but code looks correct | Yes — step through and inspect |
| Need to understand thread state during hang | Yes — pause + thread backtrace |
| `po` doesn't work / shows garbage | Yes — Playbook 3 has alternatives |
| Crash log analyzed, need to reproduce | Yes — set breakpoints from crash context |
| Need to test a fix without rebuilding | Yes — expression evaluation |
| Want to break on all exceptions | Yes — exception breakpoints |
| App feels slow but responsive | No — use axiom-performance-profiling |
| Memory grows over time | No — use axiom-memory-debugging first |
| App completely frozen | Maybe — use axiom-hang-diagnostics first, then LLDB for thread inspection |
| Crash in production, no local repro | No — use axiom-testflight-triage first |
## LLDB vs Other Tools
```dot
digraph tool_selection {
"What do you need?" [shape=diamond];
"axiom-testflight-triage" [shape=box];
"axiom-hang-diagnostics" [shape=box];
"axiom-memory-debugging" [shape=box];
"axiom-performance-profiling" [shape=box];
"LLDB (this skill)" [shape=box, style=bold];
"What do you need?" -> "axiom-testflight-triage" [label="Crash log from field,\ncan't reproduce locally"];
"What do you need?" -> "axiom-hang-diagnostics" [label="App frozen,\nneed diagnosis approach"];
"What do you need?" -> "axiom-memory-debugging" [label="Memory growing,\nneed leak pattern"];
"What do you need?" -> "axiom-performance-profiling" [label="Need to measure\nCPU/memory over time"];
"What do you need?" -> "LLDB (this skill)" [label="Need to inspect state\nat a specific moment"];
}
```
**Rule of thumb:** Instruments *measures*. LLDB *inspects*. If you need to understand what's happening at a specific moment in time, use LLDB. If you need to understand trends over time, use Instruments.
## Response Format
When helping with LLDB debugging, structure your output as:
1. **Immediate diagnosis** (1-3 bullets, confidence-tagged: HIGH/MEDIUM/LOW)
2. **Commands to run** (numbered, copy-paste ready, with `(lldb)` prefix)
3. **What to look for** (command → expected output → interpretation)
4. **Likely root causes** (ranked by probability)
5. **Next breakpoint plan** (catch it earlier next time)
6. **If no debugger attached** (crash-log-only fallback path)
---
## Playbook 1: Crash Triage
**Goal:** Understand why the app crashed, starting from the stop point.
### Step 1: Read the Stop Reason
When the debugger stops, the first thing to check:
```
(lldb) thread info
```
This shows the stop reason. Common stop reasons:
| Stop Reason | Meaning | Next Step |
|-------------|---------|-----------|
| `EXC_BAD_ACCESS (SIGSEGV)` | Accessed invalid memory (null pointer, dangling reference) | Check the address — `0x0` to `0x10` = nil dereference |
| `EXC_BAD_ACCESS (SIGBUS)` | Misaligned or invalid address | Usually C interop or unsafe pointer issue |
| `EXC_BREAKPOINT (SIGTRAP)` | Hit a trap — Swift runtime check failed | Check for `fatalError()`, `preconditionFailure()`, force-unwrap of nil, array out of bounds |
| `EXC_CRASH (SIGABRT)` | Deliberate abort — assertion or uncaught exception | Look at "Application Specific Information" for the message |
| `breakpoint` | Your breakpoint was hit | Normal — inspect state |
### Step 2: Get the Backtrace
```
(lldb) bt
```
Read top-to-bottom. Find the first frame in YOUR code (not system frameworks). That's where to start investigating.
```
(lldb) bt 10
```
Limit to 10 frames if the full trace is noisy.
### Step 3: Navigate to Your Frame
```
(lldb) frame select 3
```
Jump to frame 3 (or whichever frame is in your code).
### Step 4: Inspect State
```
(lldb) v
(lldb) v self.someProperty
(lldb) v localVariable
```
Use `v` (not `po`) for reliable Swift value inspection. See Playbook 3 for details.
### Step 5: Classify and Fix
| Exception Type | Typical Cause | Fix Pattern |
|----------------|---------------|-------------|
| `EXC_BAD_ACCESS` at low address | Force-unwrap nil optional | `guard let` / `if let` |
| `EXC_BAD_ACCESS` at high address | Use-after-free / dangling pointer | Check object lifetime, `[weak self]` |
| `EXC_BREAKPOINT` | Swift runtime trap (bounds, unwrap, precondition) | Fix the violated precondition |
| `SIGABRT` | Uncaught ObjC exception or `fatalError()` | Read the exception message, fix the root cause |
### Step 6: Set a Conditional Breakpoint to Catch It Earlier
```
(lldb) breakpoint set -f MyFile.swift -l 42 -c "value == nil"
```
This breaks only when `value` is nil at line 42 — catches the problem before the crash.
---
## Playbook 2: Hang/Deadlock Diagnosis
**Goal:** Understand why the app is frozen by inspecting all thread states.
### Step 1: Pause the App
If the app is hung, press the pause button in Xcode (⌃⌘Y) or:
```
(lldb) process interrupt
```
### Step 2: Get All Thread Backtraces
```
(lldb) thread backtrace all
```
Or the shorthand:
```
(lldb) bt all
```
### Step 3: Classify Thread States
Look at Thread 0 (main thread) — it processes all UI events. If it's blocked, the app is frozen.
**Main thread blocked on synchronous wait:**
```
frame #0: libsystem_kernel.dylib`__psynch_mutexwait
frame #1: libsystem_pthread.dylib`_pthread_mutex_firstfit_lock_wait
...
frame #5: MyApp`ViewController.viewDidLoad()
```
Translation: Main thread is waiting for a mutex lock. Something else holds it.
**Main thread blocked on dispatch_sync:**
```
frame #0: libdispatch.dylib`_dispatch_sync_f_slow
...
frame #3: MyApp`DataManager.fetchData()
```
Translation: `DispatchQueue.main.sync` called from background → classic deadlock.
**Main thread busy (CPU-bound):**
```
frame #0: MyApp`ImageProcessor.processAllImages()
frame #1: MyApp`ViewController.viewDidLoad()
```
Translation: Expensive work on main thread. Move to background.
### Step 4: Check for Deadlocks
If two threads are both waiting on something the other holds:
```
(lldb) thread list
```
Look for multiple threads with state `waiting` that reference each other's locks.
### Step 5: Inspect Specific Thread
```
(lldb) thread select 3
(lldb) bt
(lldb) v
```
Switch to another thread to inspect its state.
**Cross-reference:** For fix patterns once you've identified the hang cause → `/skill axiom-hang-diagnostics`
---
## Playbook 3: Swift Value Inspection
**This is the core value of this skill.** Most developers abandon LLDB because `po` doesn't work reliably with Swift types. Here's what actually works.
### The Four Print Commands
| Command | Full Form | What It Does | Best For |
|---------|-----------|--------------|----------|
| `v` | `frame variable` | Reads memory directly, no compilation | Swift structs, enums, locals — **your default** |
| `p` | `expression` (with formatter) | Compiles expression, shows formatted result | Computed properties, function calls |
| `po` | `expression --object-description` | Calls `debugDescription` | Classes with `CustomDebugStringConvertible` |
| `expr` | `expression` | Evaluates arbitrary code | Calling methods, modifying state |
### When to Use Each
**Start with `v`** — it's fastest and most reliable for stored properties:
```
(lldb) v self.userName
(lldb) v self.items[0]
(lldb) v localStruct
```
`v` works by reading memory directly. It doesn't compile anything, so it can't fail due to expression compilation errors.
**`v` limitation:** It only reads stored properties — computed properties, `lazy var` (before first access), and property wrapper projected values (`$binding`) won't show meaningful values. If a field looks wrong or missing with `v`, try `p` instead.
**Use `p` when `v` can't reach it:**
```
(lldb) p self.computedProperty
(lldb) p self.items.count
(lldb) p someFunction()
```
`p` compiles and executes the expression. Needed for computed properties and function calls.
**Use `po` for class descriptions:**
```
(lldb) po myObject
(lldb) po error
(lldb) po notification
```
`po` calls `debugDescription` on the result. Best for objects that have meaningful descriptions (NSError, Notification, etc.).
### The "LLDB Is Broken" Moments
| What You See | Why | Fix |
|--------------|-----|-----|
| `<uninitialized>` | `po` failed; variable hasn't been populated by optimizer | Use `v` instead |
| `expression failed to parse, unknown type name` | Swift expression parser can't resolve the type | Try `expr -l objc -- (id)0x12345` for ObjC objects, or use `v` |
| `<variable not available>` | Compiler optimized it out (Release build) | Rebuild with Debug, per-file `-Onone`, or `register read` as last resort |
| `error: Couldn't apply expression side effects` | Expression had side effects LLDB couldn't reverse | Try a simpler expression; avoid mutating state |
| `po` shows memory address instead of value | Object doesn't conform to `CustomDebugStringConvertible` | Use `v` for raw value, or implement the protocol |
| `cannot find 'self' in scope` | Breakpoint is in a context without `self` (static, closure) | Use `v` with the explicit variable name |
| `p` shows `$R0 = ...` but `po` crashes | Different compilation paths | Use `p` when it works; `po` adds an extra description step that can fail |
### Inspecting Optionals
```
(lldb) v optionalValue
```
Shows: `(String?) some = "hello"` or `(String?) none`
Don't use `po optionalValue` — it may show just `Optional("hello")` which is less useful.
### Inspecting Collections
```
(lldb) v myArray
(lldb) v myArray[2]
(lldb) v myDict
```
For large collections, limit output:
```
(lldb) p Array(myArray.prefix(5))
```
### Inspecting SwiftUI State
SwiftUI `@State` is backed by stored properties with underscore prefix:
```
(lldb) v self._isPresented
(lldb) v self._items
```
For `@Observable` models:
```
(lldb) v self.viewModel.propertyName
```
**Diagnosing "view doesn't update":** If a property changes (confirmed with `v`) but the SwiftUI view doesn't re-render, check which thread the mutation happens on with `bt`. `@Observable` mutations must happen on `@MainActor` for SwiftUI to observe them — mutations on a background actor won't trigger view updates. Use `Self._printChanges()` inside a view body to see which property triggered (or didn't trigger) a re-render:
```
(lldb) expr Self._printChanges()
```
For the full observation diagnostic tree → `/skill axiom-swiftui-debugging`
### Inspecting Actors
Actor state is best inspected with `v`, which reads memory directly without isolation concerns:
```
(lldb) v actor
```
Shows all stored properties. This works because LLDB pauses the entire process — you can read any memory regardless of actor isolation (which is a compile-time concept).
### Modifying Values at Runtime
```
(lldb) expr self.debugFlag = true
(lldb) expr myArray.append("test")
(lldb) expr self.view.backgroundColor = UIColor.red
```
Modify values without rebuilding. Useful for testing theories.
### Referencing Previous Results
LLDB assigns result variables (`$R0`, `$R1`, etc.):
```
(lldb) p someValue
$R0 = 42
(lldb) p $R0 + 10
$R1 = 52
```
---
## Playbook 4: Breakpoint Strategies
### Source Breakpoints (Basic)
```
(lldb) breakpoint set -f ViewController.swift -l 42
(lldb) b ViewController.swift:42
```
Short form `b` works for simple cases.
### Conditional Breakpoints
Break only when a condition is true:
```
(lldb) breakpoint set -f MyFile.swift -l 42 -c "index > 100"
(lldb) breakpoint set -f MyFile.swift -l 42 -c "name == \"test\""
```
**Iteration-based:** Break after N hits:
```
(lldb) breakpoint set -f MyFile.swift -l 42 -i 50
```
Ignores the first 50 hits, then breaks.
### Logpoints (Action + Auto-Continue)
Log without stopping — like a print statement but no rebuild needed:
```
(lldb) breakpoint set -f MyFile.swift -l 42
(lldb) breakpoint command add 1
> v self.value
> continue
> DONE
```
Or in Xcode: Edit breakpoint → Add Action → "Log Message" → use `@self.value@` token syntax → Check "Automatically continue"
### Symbolic Breakpoints
Break on ANY call to a method by name:
```
(lldb) breakpoint set -n viewDidLoad
(lldb) breakpoint set -n "MyClass.myMethod"
```
Break on all ObjC messages to a selector:
```
(lldb) breakpoint set -S "layoutSubviews"
```
### Exception Breakpoints
**Swift errors (break on throw):**
```
(lldb) breakpoint set -E swift
```
**Objective-C exceptions (break on throw):**
```
(lldb) breakpoint set -E objc
```
**In Xcode:** Breakpoint Navigator → + → Swift Error Breakpoint / Exception Breakpoint
This is the single most useful breakpoint for crash debugging. It stops at the throw site instead of the catch/crash site.
### Watchpoints
Break when a variable's value changes:
```
(lldb) watchpoint set variable self.count
(lldb) watchpoint set variable -w read_write myGlobal
```
Watchpoints are hardware-backed — limited to ~4 per process but very fast.
### One-Shot Breakpoints
Break once, then auto-delete:
```
(lldb) breakpoint set -f MyFile.swift -l 42 -o
```
### Managing Breakpoints
```
(lldb) breakpoint list
(lldb) breakpoint disable 3
(lldb) breakpoint enable 3
(lldb) breakpoint delete 3
(lldb) breakpoint delete
```
---
## Playbook 5: Async/Concurrency Debugging
### Identifying Async Frames
Swift concurrency backtraces are noisy — expect `swift_task_switch`, `_dispatch_call_block_and_release`, and executor internals mixed in with your code. Don't be discouraged by 40+ frames of runtime noise. Focus on frames from YOUR module.
In Swift concurrency backtraces, look for `swift-task` frames:
```
Thread 3:
frame #0: MyApp`MyActor.doWork()
frame #1: swift_task_switch
frame #2: MyApp`closure #1 in ViewController.loadData()
```
The `swift_task_switch` frame indicates an async suspension point. Your code frames are the ones prefixed with your module name (`MyApp` above).
### Inspecting Task State
```
(lldb) thread backtrace all
```
Look for threads with `swift_task` in their frames. Each represents an active Swift task.
### Actor-Isolated Code
When stopped inside an actor:
```
(lldb) v self
```
Shows all actor state. This works because LLDB pauses the entire process — actor isolation is a compile-time concept, not a runtime lock (for default actors).
### Task Group Inspection
When debugging task groups, break inside the group closure and inspect:
```
(lldb) v
(lldb) bt
```
Each child task runs on its own thread. Use `bt all` to see them.
**Cross-reference:** For Swift concurrency patterns and fix strategies → `/skill axiom-swift-concurrency`. For profiling async performance → `/skill axiom-concurrency-profiling`
---
## Pressure Scenarios
### Scenario 1: "Release-Only Crash — LLDB Is Useless in Release"
**Situation:** Crash happens in Release builds but not Debug. Team says "we can't debug it."
**Why this fails:** Release optimizations change timing, memory layout, and can eliminate variables — making the crash non-reproducible in Debug.
**Correct approach:**
1. Build with Debug configuration but Release-like settings:
- Optimization Level: `-O` (not `-Onone`)
- Still include debug symbols (`DEBUG_INFORMATION_FORMAT = dwarf-with-dsym`)
2. Enable Address Sanitizer (`-fsanitize=address`) — catches memory errors with 2-3x overhead
3. Use the crash report to set breakpoints at the crash site
4. Set exception breakpoints to catch the error before the crash:
```
(lldb) breakpoint set -E swift
(lldb) breakpoint set -E objc
```
5. If variable shows `<optimized out>`, reduce optimization for that one file:
- Build Settings → Per-file flags → `-Onone` for the specific file
6. Last resort — read register values directly (variables live in registers before being optimized out):
```
(lldb) register read
(lldb) register read x0 x1 x2
```
On ARM64: `x0` = self, `x1`-`x7` = first 7 arguments. Check `/skill axiom-lldb-ref` Part 1 for details.
### Scenario 2: "Just Add Print Statements"
**Situation:** Developer adds `print()` calls to debug, rebuilds, runs, reads console. Repeat.
**Why this fails:** Each print-debug cycle costs 3-5 minutes (edit → build → run → navigate to state → read output). An LLDB breakpoint costs 30 seconds.
**Correct approach:**
1. Set a breakpoint at the line you'd add a `print()`:
```
(lldb) b MyFile.swift:42
```
2. Add a logpoint for "print-like" behavior without rebuilding:
- Edit breakpoint → Add Action → Log Message → Check "Auto continue"
3. Inspect variables directly: `v self.someValue`
4. Modify variables at runtime to test theories: `expr self.debugMode = true`
5. **One breakpoint session replaces 5-10 print-debug cycles.**
**Time comparison** (typical control-flow debugging):
| Approach | Per investigation | 5 variables |
|----------|-------------------|-------------|
| print() statements | 3-5 min (build + run) | 15-25 min |
| LLDB breakpoint | 30 sec (set + inspect) | 2.5 min |
**Exception:** In tight loops (thousands of hits/sec), logpoints add per-hit overhead. Use `-i` to skip to the iteration you care about, or use a temporary `print()` for that specific loop.
### Scenario 3: "po Doesn't Work So LLDB Is Broken"
**Situation:** Developer types `po myStruct` and gets garbage. Concludes LLDB is broken for Swift. Goes back to print debugging.
**This is the #1 reason developers abandon LLDB.**
**Why `po` fails with Swift structs:** `po` calls `debugDescription` which requires compiling an expression in the debugger context. For Swift structs, this compilation often fails due to missing type metadata, generics, or module resolution issues.
**Correct approach:**
1. Use `v` instead of `po` — reads memory directly, no compilation:
```
(lldb) v myStruct
(lldb) v myStruct.propertyName
```
2. Use `p` for computed properties:
```
(lldb) p myStruct.computedValue
```
3. Use `po` only for classes with `CustomDebugStringConvertible`
4. If `p` also fails, try specifying the language:
```
(lldb) expr -l objc -- (id)0x12345
```
5. If everything fails, `v self` always works inside a method.
---
## Anti-Patterns
| Anti-Pattern | Why It's Wrong | Better Alternative |
|---|---|---|
| `po` everything | Fails for Swift structs, enums, optionals | `v` for values, `po` only for classes |
| Print-debug cycles | 3-5 min per cycle vs 30 sec breakpoint | Breakpoints with logpoint actions |
| "LLDB doesn't work with Swift" | It does — wrong command choice | `v` is designed for Swift values |
| Ignoring backtraces | Jumping to guesses instead of reading the trace | `bt` first, then navigate frames |
| Conditional breakpoints on every hit | Slows execution if condition is expensive | Use `-i` (ignore count) when possible |
| Debugging optimized (Release) builds | Variables missing, code reordered | Debug configuration, or per-file `-Onone` |
| Force-continuing past exceptions | Hides the real error | Fix the exception, don't suppress it |
| No exception breakpoints set | Crashes land in system code, not throw site | Always add Swift Error + ObjC Exception breakpoints |
## Debugging Checklist
Before starting a debug session:
- [ ] Debug build configuration (not Release)
- [ ] Exception breakpoints enabled (Swift Error + ObjC Exception)
- [ ] Breakpoint set before suspected problem area
- [ ] Know which command to use: `v` for values, `p` for computed, `po` for descriptions
During debug session:
- [ ] Read stop reason (`thread info`) before anything else
- [ ] Get backtrace (`bt`) — find your frame
- [ ] Navigate to your frame (`frame select N`)
- [ ] Inspect relevant state (`v self`, `v localVar`)
- [ ] Understand the cause before writing any fix
After finding the issue:
- [ ] Set conditional breakpoint to catch recurrence
- [ ] Consider adding assertion/precondition for this case
- [ ] Remove temporary breakpoints
## Resources
**WWDC**: 2019-429, 2018-412, 2022-110370
**Docs**: /xcode/stepping-through-code-and-inspecting-variables-to-isolate-bugs, /xcode/setting-breakpoints-to-pause-your-running-app, /xcode/diagnosing-memory-thread-and-crash-issues-early
**Skills**: axiom-lldb-ref, axiom-testflight-triage, axiom-hang-diagnostics, axiom-memory-debugging, axiom-swift-concurrency, axiom-concurrency-profiling