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

108 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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`:
```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 12 (small macOS + iOS-sim binaries) are still
verified; this gap is specific to large builds' debug map.