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.
This commit is contained in:
agra
2026-06-01 16:47:51 +03:00
parent b2ebf774bc
commit 0d7f786db2
3 changed files with 117 additions and 3 deletions

View File

@@ -989,7 +989,10 @@ test "emit: ERR E3.0 — DWARF debug info (compile unit + subprogram + per-inst
try std.testing.expect(std.mem.indexOf(u8, ir_str, "\"Debug Info Version\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "\"Dwarf Version\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DICompileUnit") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DIFile(filename: \"probe.sx\"") != null);
// Regression (issue 0058): a bare filename (no directory component) must
// still get a NON-EMPTY `directory:` — an empty `DW_AT_comp_dir` makes ld
// silently drop the whole debug map, so the binary becomes undebuggable.
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DIFile(filename: \"probe.sx\", directory: \".\")") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DISubprogram(name: \"main\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DILocation(line: 3") != null);
}

View File

@@ -394,11 +394,15 @@ pub const LLVMEmitter = struct {
}
/// The `DIFile` for `path`, created once and cached. Splits the path
/// into basename + directory as DWARF expects.
/// into basename + directory as DWARF expects. The directory MUST be
/// non-empty: an empty `DW_AT_comp_dir` makes Apple's `ld` silently drop
/// the whole object's debug map (no `N_OSO`), so a binary built from a
/// bare filename (e.g. `sx build main.sx`) becomes undebuggable. Fall back
/// to "." when the path has no directory component.
fn diFileFor(self: *LLVMEmitter, path: []const u8) c.LLVMMetadataRef {
if (self.di_files.get(path)) |f| return f;
const slash = std.mem.lastIndexOfScalar(u8, path, '/');
const dir = if (slash) |s| path[0..s] else "";
const dir = if (slash) |s| (if (s == 0) "/" else path[0..s]) else ".";
const base = if (slash) |s| path[s + 1 ..] else path;
const f = c.LLVMDIBuilderCreateFile(self.di_builder, base.ptr, base.len, dir.ptr, dir.len);
self.di_files.put(path, f) catch {};