Files
sx/issues/0058-empty-debug-map-large-build.md
agra 0d7f786db2 fix(dwarf): non-empty comp_dir so ld keeps the debug map (issue 0058)
A source path with no directory component (`sx build main.sx` from the
project dir — what the chess app does) made `diFileFor` emit a `DIFile`
with an empty `directory:`, so the compile unit's `DW_AT_comp_dir` was
"". Apple's ld then silently drops the *entire* object's debug map (0
N_OSO) and the binary is undebuggable — lldb resolves no sx source.
Builds whose path had any directory (`.sx-tmp/x.sx`, `examples/x.sx`)
were unaffected, which is why small repros + the stepping smoke passed
and only the bundled chess app 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.

Verified: chess (`sx build --target macos --emit-obj main.sx`) now
links with OSO=1 and lldb resolves `frame at main.sx:82:8`. Regression
guard added to the DWARF unit test (asserts `DIFile(... directory: ".")`
for a bare filename). Gates: zig build, zig build test, run_examples.sh
-> 291 passed, debug-stepping smoke ok.
2026-06-01 16:47:51 +03:00

5.6 KiB
Raw Permalink Blame History

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, noat 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 12) 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:

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 emissionsrc/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 framemain.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 12 (small macOS + iOS-sim binaries) are still verified; this gap is specific to large builds' debug map.