diff --git a/docs/debugger.md b/docs/debugger.md index 06ae747..871ee6d 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -389,9 +389,12 @@ emit; sx provides the artifacts and a launch convenience, nothing more. ### Artifacts -`sx build --emit-obj` / `--debug` writes the object (+DWARF) to a build -dir and runs `dsymutil` to produce a `.dSYM`, so a debugger can load and -symbolize it. Reuses the existing `emitObject` path. +`sx build --emit-obj` keeps the DWARF-bearing object at its link-time path +(`.sx-tmp/main.o`) instead of deleting it, and implies `-O0` (DWARF only emits +at opt none/less). On **macOS** the linked binary's debug map resolves to that +`.o`, so `lldb`/`gdb` run from the project root can step the binary directly; on +**Linux** the DWARF is in the binary, so the `.o` isn't even needed. A portable +`.dSYM` (via `dsymutil`) is only required for the on-device iOS rung (below). ### The verification ladder @@ -400,8 +403,9 @@ Source-level stepping is verified manually/interactively (it needs provisioning profile — not a `run_examples.sh` test). Climb cheapest-first; the device run is the final sign-off: -1. **macOS native** — `sx build --opt none` → `dsymutil` → drive - `lldb --batch` with a canned script: breakpoint on a sx function, +1. **macOS native ✅** — `sx build --emit-obj` → drive `lldb --batch` (the + debug map resolves to the kept `.o`; no `dsymutil` needed locally). + Checked in as `tests/debug_stepping_smoke.sh`: breakpoint on a sx function, `run`, assert it stops at the right `.sx:line`, `next`/`stepi` advance, `bt` is source-mapped. The automatable rung (a checked-in smoke script). 2. **iOS simulator** — bundle the `.app`, install to a booted simulator @@ -438,8 +442,9 @@ a Mach-O debug map, never register JIT DWARF. | Niladic trace-push op + interned `Frame` table (runtime) | ✅ done — E3.3 slice 3a (`1b6cbc1`) | | Comptime resolver (`func_id, ir_offset` → location) | ✅ done — slice 3b | | Source snippet + `^` caret | ✅ done — slice 3c (line embedded in `Frame`) | -| `--emit-obj` / `--debug` artifact plumbing | ⏳ planned — slice 3d | -| Stepping verification ladder (macOS → sim → device) | ⏳ planned — slice 3e (capstone) | +| `--emit-obj` artifact plumbing | ✅ done — slice 3d | +| Stepping verification: macOS lldb | ✅ done — slice 3e rung 1 (`tests/debug_stepping_smoke.sh`) | +| Stepping verification: iOS simulator → device | ⏳ planned — slice 3e rungs 2–3 (capstone) | | DWARF variable info (`DILocalVariable`, for `p x`) | ⏳ optional follow-on | The active plan and step breakdown live in `current/PLAN-ERR.md` diff --git a/src/main.zig b/src/main.zig index 83a1cf2..21bcc52 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 .mobileprovision to embed (required for device) \\ --entitlements 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 { diff --git a/src/target.zig b/src/target.zig index ab6b56b..0ec72b2 100644 --- a/src/target.zig +++ b/src/target.zig @@ -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, diff --git a/tests/debug_stepping_smoke.sh b/tests/debug_stepping_smoke.sh new file mode 100755 index 0000000..003a10c --- /dev/null +++ b/tests/debug_stepping_smoke.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Debug-stepping smoke (ERR E3.0 slice 3e, rung 1: macOS native). +# +# Verifies the DWARF emitted by `sx build --emit-obj` actually drives +# source-level stepping in lldb — the deep-debug half of the trace story. +# NOT part of `run_examples.sh` (it needs `lldb`, and is macOS-specific via the +# debug-map → kept `.o`). Run manually: bash tests/debug_stepping_smoke.sh +# +# Exit 0 = lldb resolved a file:line breakpoint + a source-mapped backtrace. + +set -u +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SX="$ROOT_DIR/zig-out/bin/sx" +TMP="$ROOT_DIR/.sx-tmp" +SRC="$TMP/dbg_smoke.sx" +BIN="$TMP/dbg_smoke" + +if ! command -v lldb >/dev/null 2>&1; then + echo "SKIP: lldb not found (macOS/Xcode tools required)" + exit 0 +fi + +mkdir -p "$TMP" +cat > "$SRC" <<'EOF' +add :: (a: s32, b: s32) -> s32 { + c := a + b; + return c; +} +main :: () -> s32 { + return add(40, 2); +} +EOF + +"$SX" build --emit-obj "$SRC" -o "$BIN" >/dev/null 2>&1 || { echo "FAIL: build"; exit 1; } + +# Breakpoint on the `return c;` line; expect lldb to resolve it + a backtrace +# mapping both frames to dbg_smoke.sx. +out=$(cd "$ROOT_DIR" && lldb --batch \ + -o "breakpoint set --file dbg_smoke.sx --line 3" \ + -o "run" -o "bt" -o "quit" "$BIN" 2>&1) + +rm -f "$SRC" "$BIN" "$TMP/main.o" + +fail=0 +echo "$out" | grep -q "dbg_smoke.sx:3" || { echo "FAIL: breakpoint did not resolve to dbg_smoke.sx:3"; fail=1; } +echo "$out" | grep -q "add at dbg_smoke.sx:3" || { echo "FAIL: stopped frame not source-mapped"; fail=1; } +echo "$out" | grep -q "main at dbg_smoke.sx:6" || { echo "FAIL: caller frame not source-mapped"; fail=1; } + +if [[ $fail -eq 0 ]]; then + echo "ok: lldb stepped sx source (breakpoint + backtrace resolved via DWARF)" + exit 0 +fi +echo "--- lldb output ---"; echo "$out" +exit 1 diff --git a/tests/expected/253-comptime-trace.exit b/tests/expected/253-comptime-trace.exit index c227083..573541a 100644 --- a/tests/expected/253-comptime-trace.exit +++ b/tests/expected/253-comptime-trace.exit @@ -1 +1 @@ -0 \ No newline at end of file +0