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.
5.6 KiB
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 showswhere = SxChess\frame, … unresolved(symbol present, noat main.sx:line). Adsymutil.dSYM` is therefore empty (UUID matches, but no line info). - Expected: an
N_OSOdebug-map entry formain.o(like every small build gets), so lldb resolvesfunc at main.sx:lineand steps sx source.
What's confirmed / ruled out
main.ohas valid DWARF:llvm-dwarfdump --debug-line .sx-tmp/main.oshowsmain.sx;--debug-infoshowsDW_TAG_compile_unit DW_AT_name "main.sx", DWARF32 v4. So DWARF emission (ERR E3.0 slices 1–2) is fine and--emit-objcorrectly 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 keepsOSO=1; the binary islinker-signedby ld anyway); the bundler (the pre-bundlesx-out/macos/SxChessalready has 0 OSO, andbundle.sxruns nostrip); opt level (DWARF is present in the .o);#import "modules/std.sx"(a tiny std-importing program keepsOSO=1); missing global_main(chess does haveT _main). - The remaining correlate: chess's
main.ois large (~1.1 MB, many monomorphized functions merged from many imported modules). Every small repro tried keepsOSO=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:
- Object emission —
src/ir/emit_llvm.zigemitObject/LLVMTargetMachineEmitToFile. Diff the working smallmain.ovs the chessmain.o: do both have the same Mach-O symbol-table shape (N_FUN/global anchors),__DWARFsection set, and__debug_aranges/__debug_linelayout? A large single compile unit, a DWARF feature ld dislikes (e.g..debug_names,DW_FORMld can't follow), or a section-size/symbol threshold could make ld skip the debug map for that object. - Minimize: grow a single-file program (more functions / import more
modules / pull in
platform/bundle.sx) untildsymutil -dump-debug-mapflips from 1 → 0. That pins the trigger (size? a specific module/construct?). - Check whether ld prints a (suppressed) warning — run the link with
SX_DEBUG_LINK=1and-Wl,-debug_variant/ ld verbosity, or link the chess objects by hand withld -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.