ERR/E3.0 (slice 3d): --emit-obj + macOS lldb stepping verified

`sx build --emit-obj` keeps the DWARF-bearing object so a debugger can
step the binary, completing the deep-debug half of the trace story.

- --emit-obj flag + TargetConfig.emit_obj. Implies -O0 (DWARF only
  emits at opt none/less); keeps the object at its link-time path
  .sx-tmp/main.o so the binary's debug map resolves to it; skips the
  Level-1 binary cache; reports the object path. macOS resolves via the
  debug map -> .o; Linux carries DWARF in the binary. Build-flow only,
  no runtime/codegen change.
- tests/debug_stepping_smoke.sh (3e rung 1; macOS, lldb, not in
  run_examples): builds with --emit-obj, drives an lldb file:line
  breakpoint, asserts resolution + a source-mapped backtrace. Passing —
  proves the slice 1-2 DWARF drives real source-level stepping.

(Also normalizes the 253 .exit trailing newline from the 3c --update.)
Gates: zig build, zig build test, run_examples.sh -> 291 passed.
This commit is contained in:
agra
2026-06-01 15:55:05 +03:00
parent 178449b548
commit 4cd641c946
5 changed files with 92 additions and 13 deletions

View File

@@ -128,6 +128,8 @@ pub fn main(init: std.process.Init) !void {
show_timing = true;
} else if (std.mem.eql(u8, arg, "--cache")) {
enable_cache = true;
} else if (std.mem.eql(u8, arg, "--emit-obj")) {
target_config.emit_obj = true;
} else if (std.mem.startsWith(u8, arg, "-L")) {
if (arg.len > 2) {
try lib_paths.append(allocator, arg[2..]);
@@ -183,6 +185,9 @@ pub fn main(init: std.process.Init) !void {
if (std.mem.eql(u8, command, "build")) {
target_config.is_aot = true;
// `--emit-obj` keeps a debuggable object; DWARF only emits at opt
// none/less, so default to -O0 unless the user set --opt explicitly.
if (target_config.emit_obj and !explicit_opt) target_config.opt_level = .none;
const output_name = target_config.output_path orelse blk: {
const base = deriveOutputName(path);
if (target_config.isEmscripten()) {
@@ -390,6 +395,7 @@ fn printUsage() void {
\\ --provisioning-profile <path> .mobileprovision to embed (required for device)
\\ --entitlements <path> Entitlements plist (auto-extracted from profile if omitted)
\\ --cache Enable build caching
\\ --emit-obj Keep the debuggable object (DWARF; implies -O0) for lldb/gdb
\\ --time Show compilation timing breakdown
\\
, .{});
@@ -570,8 +576,9 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
const cache_obj = try cachePath(allocator, key, "o");
const cache_bin = try cachePath(allocator, key, "bin");
// Level 1: Try cached binary (skip everything — no codegen, no link)
if (enable_cache) bin_cache: {
// Level 1: Try cached binary (skip everything — no codegen, no link).
// Skipped under --emit-obj, which needs the freshly-emitted object kept.
if (enable_cache and !target_config.emit_obj) bin_cache: {
std.Io.Dir.copyFile(.cwd(), cache_bin, .cwd(), output_path, io, .{}) catch break :bin_cache;
timer.record("cache");
return;
@@ -776,14 +783,20 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
std.debug.print("compiled: {s}\n", .{final_output});
// Clean up temp directory and all build artifacts
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
// Clean up temp directory and all build artifacts. Under --emit-obj, keep
// the object (DWARF for lldb/gdb) at its link-time path — the binary's
// debug map resolves to it — and skip removing the temp dir.
const shell_tmp = std.fmt.allocPrint(allocator, "{s}.shell.html", .{obj_path}) catch null;
if (shell_tmp) |sp| std.Io.Dir.deleteFile(.cwd(), io, sp) catch {};
for (c_obj_paths) |cop| {
std.Io.Dir.deleteFile(.cwd(), io, cop) catch {};
}
std.Io.Dir.deleteDir(.cwd(), io, tmp_dir) catch {};
if (target_config.emit_obj) {
std.debug.print("debug object kept: {s} (DWARF; run lldb/gdb from the project root)\n", .{obj_path});
} else {
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
std.Io.Dir.deleteDir(.cwd(), io, tmp_dir) catch {};
}
}
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool, stdlib_paths: []const []const u8) !void {

View File

@@ -71,6 +71,13 @@ pub const TargetConfig = struct {
/// `chdir` shouldn't run in JIT mode because it would mutate the host
/// sx process's CWD.
is_aot: bool = false,
/// Keep the DWARF-bearing object file after linking (`--emit-obj`) so a
/// debugger can step the binary: macOS resolves via the debug map → the
/// `.o`; Linux carries DWARF in the binary directly. Implies `-O0` unless
/// `--opt` is given explicitly (DWARF is only emitted at opt none/less).
/// The object is kept at `.sx-tmp/main.o` (its link-time path, so the
/// debug map resolves when lldb is run from the project root).
emit_obj: bool = false,
pub const OptLevel = enum {
none,