# 0058 — large/bundled macOS build links with an empty DWARF debug map ## ✅ RESOLVED (2026-06-01) Root cause: **an empty `DW_AT_comp_dir`**. A source path with no directory component (`sx build main.sx` from the project dir) made `emit_llvm`'s `diFileFor` emit a `DIFile` with an empty `directory:`, so the compile unit's `comp_dir` was `""`. Apple's `ld` then silently drops the *entire* object's debug map (no `N_OSO`) — the binary becomes undebuggable. Builds whose path had any directory component (`.sx-tmp/x.sx`, `examples/x.sx`) were unaffected, which is why small repros + the smoke passed and only the chess app (`sx build main.sx`) hit it. Fix: `diFileFor` falls back to `"."` (and `/` for a root-level file) when the path has no directory component, so `comp_dir` is never empty. One-line change in `src/ir/emit_llvm.zig`. Regression guard added to the DWARF unit test in `src/ir/emit_llvm.test.zig` (asserts `DIFile(... directory: ".")` for a bare filename). Verified: chess (`sx build --target macos --emit-obj main.sx`) now links with `OSO: 1` and lldb resolves `frame at main.sx:82:8`. --- ## Symptom One-line: `sx build --emit-obj` produces a **debuggable** binary for small sx programs (lldb resolves `.sx:line`), but for the chess app (a large, multi-module bundled macOS build) the linked binary has an **empty debug map** (0 `N_OSO` entries) even though `main.o` carries valid DWARF — so lldb / a VSCode debug session cannot resolve any sx source for the real app. - **Observed:** `dsymutil -dump-debug-map sx-out/macos/SxChess` → `---` (no objects). `nm -ap … | grep -c ' OSO '` → 0. lldb shows `where = SxChess\`frame, … unresolved` (symbol present, no `at main.sx:line`). A `dsymutil` `.dSYM` is therefore empty (UUID matches, but no line info). - **Expected:** an `N_OSO` debug-map entry for `main.o` (like every small build gets), so lldb resolves `func at main.sx:line` and steps sx source. ## What's confirmed / ruled out - `main.o` **has** valid DWARF: `llvm-dwarfdump --debug-line .sx-tmp/main.o` shows `main.sx`; `--debug-info` shows `DW_TAG_compile_unit DW_AT_name "main.sx"`, DWARF32 v4. So DWARF emission (ERR E3.0 slices 1–2) is fine and `--emit-obj` correctly forces `-O0`. - The link command is clean — `cc -o out -lc -lobjc -framework Foundation -framework Metal -L/opt/homebrew/lib -lSDL3`. **No** `-S` / `-dead_strip` / `strip` / `-g` / explicit linker override. - NOT caused by: the framework/lib set (linking a small DWARF object with the exact same frameworks keeps `OSO=1`); ad-hoc **codesign** (signing a small binary keeps `OSO=1`; the binary is `linker-signed` by ld anyway); the **bundler** (the *pre-bundle* `sx-out/macos/SxChess` already has 0 OSO, and `bundle.sx` runs no `strip`); **opt level** (DWARF is present in the .o); `#import "modules/std.sx"` (a tiny std-importing program keeps `OSO=1`); missing **global `_main`** (chess *does* have `T _main`). - The remaining correlate: chess's `main.o` is **large** (~1.1 MB, many monomorphized functions merged from many imported modules). Every small repro tried keeps `OSO=1`; only the large multi-module build drops it. A minimal repro has NOT been isolated yet. ## Reproduction Not yet minimal (this is part of the fix). Reproduces reliably on the chess app at `~/projects/game`: ```sh cd ~/projects/game /path/to/sx build --target macos --emit-obj main.sx dsymutil -dump-debug-map sx-out/macos/SxChess # → `---` (empty); BUG llvm-dwarfdump --debug-line .sx-tmp/main.o | grep main.sx # DWARF IS present ``` Small programs (single file, with or without `#import "modules/std.sx"`, with or without the chess framework set) all produce `OSO: 1` and step fine in lldb. ## Investigation prompt The Mach-O **debug map** (`N_OSO`/`N_FUN` stabs) is synthesized by Apple's `ld` at link time from each object's DWARF + symbol table; sx does not emit it directly. ld is silently emitting **none** for the large chess `main.o`. Suspected areas: 1. **Object emission** — `src/ir/emit_llvm.zig` `emitObject` / `LLVMTargetMachineEmitToFile`. Diff the working small `main.o` vs the chess `main.o`: do both have the same Mach-O symbol-table shape (N_FUN/global anchors), `__DWARF` section set, and `__debug_aranges`/`__debug_line` layout? A large single compile unit, a DWARF feature ld dislikes (e.g. `.debug_names`, `DW_FORM` ld can't follow), or a section-size/symbol threshold could make ld skip the debug map for that object. 2. **Minimize**: grow a single-file program (more functions / import more modules / pull in `platform/bundle.sx`) until `dsymutil -dump-debug-map` flips from 1 → 0. That pins the trigger (size? a specific module/construct?). 3. Check whether ld prints a (suppressed) warning — run the link with `SX_DEBUG_LINK=1` and `-Wl,-debug_variant` / ld verbosity, or link the chess objects by hand with `ld -v`. Likely fix is in how we emit the object's DWARF/symbol table (so ld accepts it), or emitting our own `.dSYM`-compatible companion. The cheap workaround for users until fixed: debug a small sx repro of the failing logic, or emit per- module objects. Verification: `dsymutil -dump-debug-map ` lists `main.o`, and `lldb` resolves `frame` → `main.sx:line`. ## Impact Blocks source-level **debugging of real (large/bundled) sx apps** in lldb / VSCode. The trace-formatting feature (ERR E3) is unaffected — runtime + comptime return traces resolve in-process via the embedded `Frame` table (no debug map needed). ERR E3 stepping rungs 1–2 (small macOS + iOS-sim binaries) are still verified; this gap is specific to large builds' debug map.