# 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 <objs> -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 <chess-binary>` 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.
