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:
@@ -389,9 +389,12 @@ emit; sx provides the artifacts and a launch convenience, nothing more.
|
|||||||
|
|
||||||
### Artifacts
|
### Artifacts
|
||||||
|
|
||||||
`sx build --emit-obj` / `--debug` writes the object (+DWARF) to a build
|
`sx build --emit-obj` keeps the DWARF-bearing object at its link-time path
|
||||||
dir and runs `dsymutil` to produce a `.dSYM`, so a debugger can load and
|
(`.sx-tmp/main.o`) instead of deleting it, and implies `-O0` (DWARF only emits
|
||||||
symbolize it. Reuses the existing `emitObject` path.
|
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
|
### 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;
|
provisioning profile — not a `run_examples.sh` test). Climb cheapest-first;
|
||||||
the device run is the final sign-off:
|
the device run is the final sign-off:
|
||||||
|
|
||||||
1. **macOS native** — `sx build --opt none` → `dsymutil` → drive
|
1. **macOS native ✅** — `sx build --emit-obj` → drive `lldb --batch` (the
|
||||||
`lldb --batch` with a canned script: breakpoint on a sx function,
|
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,
|
`run`, assert it stops at the right `.sx:line`, `next`/`stepi` advance,
|
||||||
`bt` is source-mapped. The automatable rung (a checked-in smoke script).
|
`bt` is source-mapped. The automatable rung (a checked-in smoke script).
|
||||||
2. **iOS simulator** — bundle the `.app`, install to a booted simulator
|
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`) |
|
| Niladic trace-push op + interned `Frame` table (runtime) | ✅ done — E3.3 slice 3a (`1b6cbc1`) |
|
||||||
| Comptime resolver (`func_id, ir_offset` → location) | ✅ done — slice 3b |
|
| Comptime resolver (`func_id, ir_offset` → location) | ✅ done — slice 3b |
|
||||||
| Source snippet + `^` caret | ✅ done — slice 3c (line embedded in `Frame`) |
|
| Source snippet + `^` caret | ✅ done — slice 3c (line embedded in `Frame`) |
|
||||||
| `--emit-obj` / `--debug` artifact plumbing | ⏳ planned — slice 3d |
|
| `--emit-obj` artifact plumbing | ✅ done — slice 3d |
|
||||||
| Stepping verification ladder (macOS → sim → device) | ⏳ planned — slice 3e (capstone) |
|
| 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 |
|
| DWARF variable info (`DILocalVariable`, for `p x`) | ⏳ optional follow-on |
|
||||||
|
|
||||||
The active plan and step breakdown live in `current/PLAN-ERR.md`
|
The active plan and step breakdown live in `current/PLAN-ERR.md`
|
||||||
|
|||||||
23
src/main.zig
23
src/main.zig
@@ -128,6 +128,8 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
show_timing = true;
|
show_timing = true;
|
||||||
} else if (std.mem.eql(u8, arg, "--cache")) {
|
} else if (std.mem.eql(u8, arg, "--cache")) {
|
||||||
enable_cache = true;
|
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")) {
|
} else if (std.mem.startsWith(u8, arg, "-L")) {
|
||||||
if (arg.len > 2) {
|
if (arg.len > 2) {
|
||||||
try lib_paths.append(allocator, arg[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")) {
|
if (std.mem.eql(u8, command, "build")) {
|
||||||
target_config.is_aot = true;
|
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 output_name = target_config.output_path orelse blk: {
|
||||||
const base = deriveOutputName(path);
|
const base = deriveOutputName(path);
|
||||||
if (target_config.isEmscripten()) {
|
if (target_config.isEmscripten()) {
|
||||||
@@ -390,6 +395,7 @@ fn printUsage() void {
|
|||||||
\\ --provisioning-profile <path> .mobileprovision to embed (required for device)
|
\\ --provisioning-profile <path> .mobileprovision to embed (required for device)
|
||||||
\\ --entitlements <path> Entitlements plist (auto-extracted from profile if omitted)
|
\\ --entitlements <path> Entitlements plist (auto-extracted from profile if omitted)
|
||||||
\\ --cache Enable build caching
|
\\ --cache Enable build caching
|
||||||
|
\\ --emit-obj Keep the debuggable object (DWARF; implies -O0) for lldb/gdb
|
||||||
\\ --time Show compilation timing breakdown
|
\\ --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_obj = try cachePath(allocator, key, "o");
|
||||||
const cache_bin = try cachePath(allocator, key, "bin");
|
const cache_bin = try cachePath(allocator, key, "bin");
|
||||||
|
|
||||||
// Level 1: Try cached binary (skip everything — no codegen, no link)
|
// Level 1: Try cached binary (skip everything — no codegen, no link).
|
||||||
if (enable_cache) bin_cache: {
|
// 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;
|
std.Io.Dir.copyFile(.cwd(), cache_bin, .cwd(), output_path, io, .{}) catch break :bin_cache;
|
||||||
timer.record("cache");
|
timer.record("cache");
|
||||||
return;
|
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});
|
std.debug.print("compiled: {s}\n", .{final_output});
|
||||||
|
|
||||||
// Clean up temp directory and all build artifacts
|
// Clean up temp directory and all build artifacts. Under --emit-obj, keep
|
||||||
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
|
// 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;
|
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 {};
|
if (shell_tmp) |sp| std.Io.Dir.deleteFile(.cwd(), io, sp) catch {};
|
||||||
for (c_obj_paths) |cop| {
|
for (c_obj_paths) |cop| {
|
||||||
std.Io.Dir.deleteFile(.cwd(), io, cop) catch {};
|
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 {
|
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 {
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ pub const TargetConfig = struct {
|
|||||||
/// `chdir` shouldn't run in JIT mode because it would mutate the host
|
/// `chdir` shouldn't run in JIT mode because it would mutate the host
|
||||||
/// sx process's CWD.
|
/// sx process's CWD.
|
||||||
is_aot: bool = false,
|
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 {
|
pub const OptLevel = enum {
|
||||||
none,
|
none,
|
||||||
|
|||||||
54
tests/debug_stepping_smoke.sh
Executable file
54
tests/debug_stepping_smoke.sh
Executable file
@@ -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
|
||||||
Reference in New Issue
Block a user