commit 48f724456723e62594cc35cef5b5a9bab33c61ba parent 2e4a28a4b42fbbc07eb68dd16b689b5bd1b77fcf Author: Ryan Sepassi <rsepassi@gmail.com> Date: Thu, 28 May 2026 21:12:33 -0700 dbg: add disasm, expand tests Diffstat:
59 files changed, 493 insertions(+), 71 deletions(-)
diff --git a/doc/DBG_TODO.md b/doc/DBG_TODO.md @@ -9,8 +9,8 @@ experience is solid. ## Current Shape - `cfree dbg` has a real REPL in `driver/dbg.c`: `run`, `cont`, stepping, - breakpoints, `jit`, `expr`, `bt`, `p`, `set`, `x`, `list`, and `info` - commands. + breakpoints, `jit`, `expr`, `bt`, `p`, `set`, `x`, `disasm`, `list`, and + `info` commands. - `src/dbg/` owns the JIT session, worker thread, signal stop/resume loop, breakpoint patching, guarded memory access, displaced stepping, and source-level resume modes. @@ -63,16 +63,19 @@ experience is solid. transcript is in place) - [x] `:language toy` switching from a non-Toy default - [ ] Add debugger-control Toy coverage for: - - [ ] `b sym`, `run`, `cont` - - [ ] `b file:line` - - [ ] `s`, `sl`, `next`, `finish` - - [ ] `bt` - - [ ] `info reg` + - [x] `b sym`, `run`, `cont` + - [x] `b file:line` + - [x] `s`, `next` + - [ ] `sl`, `finish` + - [x] `bt` + - [x] `info reg` - [ ] `p`, `set`, `info locals`, `info args` - - [ ] `x ADDR [count]` + (`toy-local-inspection` red transcript is in place; Toy needs + usable local/argument DWARF locations) + - [x] `x ADDR [count]` - [ ] Ctrl-C/interrupt behavior where the host can test it reliably - [ ] Improve Toy REPL output: - - [ ] quieter successful `jit` output, or a mode to hide generation spam + - [x] quieter successful `jit` output; generation spam is hidden - [ ] typed result formatting instead of always `u64/i64` style - [ ] richer expression thunk signatures so expressions can accept and return non-integer values, including pointers, floats, records, @@ -125,7 +128,7 @@ experience is solid. - [ ] Print a compact source context after source-level stops. - [ ] Make stop messages distinguish user breakpoints, internal step completions, signals, traps, and program exits cleanly. -- [ ] Add `disasm` / `x/i` around the current PC. +- [x] Add `disasm` / `x/i` around the current PC. - [ ] Add memory-format variants for `x` (bytes, words, strings, pointers). - [ ] Add stable machine-readable transcript mode for tests and tools. - [ ] Keep command parsing factored enough that future editor/IDE frontends can @@ -134,7 +137,7 @@ experience is solid. ## Robustness And Portability -- [ ] Fix Darwin/arm64 sanitizer build breakpointing: `ucontext_t` access in +- [x] Fix Darwin/arm64 sanitizer build breakpointing: `ucontext_t` access in the signal handler currently trips UBSan on misalignment in debug builds. - [ ] Add direct tests for breakpoint patch/restore and read overlay. - [ ] Add direct tests for guarded bad memory reads. diff --git a/driver/dbg.c b/driver/dbg.c @@ -2,6 +2,7 @@ #include <cfree/compile.h> #include <cfree/core.h> #include <cfree/dbg.h> +#include <cfree/disasm.h> #include <cfree/dwarf.h> #include <cfree/jit.h> #include <cfree/link.h> @@ -129,6 +130,7 @@ void driver_help_dbg(void) { " p NAME print variable / global\n" " set NAME VALUE write VALUE into NAME\n" " x ADDR [count] examine memory (default 16 bytes)\n" + " disasm [ADDR] [count], x/i disassemble at PC or ADDR\n" " list FILE:LINE | l FILE:LINE source listing around FILE:LINE\n" " info b list breakpoints\n" " info reg, info registers dump registers\n" @@ -1736,9 +1738,6 @@ static int dbg_jit_compile_append_ex(DbgState* s, CfreeLanguage lang, return 1; } dbg_refresh_dwarf(s); - driver_printf("JIT generation %llu (%.*s)\n", - (unsigned long long)cfree_jit_generation(s->jit), - CFREE_SLICE_ARG(cfree_slice_cstr(dbg_jit_language_name(lang)))); return 0; } @@ -2102,6 +2101,65 @@ static void dbg_cmd_examine(DbgState* s, uint64_t addr, size_t count) { } /* ============================================================ + * `disasm [ADDR] [count]`, `x/i [ADDR] [count]` + * ============================================================ + * Decode a small instruction window from the stopped program. Without an + * address, starts at the stopped PC; count is in instructions. */ + +static void dbg_cmd_disasm(DbgState* s, uint64_t addr, size_t count) { + uint8_t buf[512]; + CfreeDisasmContext dctx; + CfreeDisasmIter* it = NULL; + size_t byte_count; + size_t shown = 0; + + if (!s->has_stop) { + driver_errf(DBG_TOOL, "no program is stopped"); + return; + } + if (count == 0) count = 8; + if (count > 32) { + driver_errf(DBG_TOOL, "disasm count too large"); + return; + } + byte_count = count * 16u; + if (byte_count > sizeof(buf)) byte_count = sizeof(buf); + if (cfree_jit_session_read_mem(s->session, addr, buf, byte_count) != + CFREE_OK) { + driver_errf(DBG_TOOL, "read failed at 0x%llx", (unsigned long long)addr); + return; + } + + memset(&dctx, 0, sizeof(dctx)); + dctx.target = driver_host_target(); + dctx.context = s->ctx; + if (cfree_disasm_iter_new(&dctx, buf, byte_count, addr, s->view, &it) != + CFREE_OK) { + driver_errf(DBG_TOOL, "disassembler unavailable"); + return; + } + while (shown < count) { + CfreeInsn insn; + CfreeIterResult r = cfree_disasm_iter_next(it, &insn); + if (r == CFREE_ITER_END) break; + if (r != CFREE_ITER_ITEM) { + driver_errf(DBG_TOOL, "disassembly failed"); + break; + } + driver_printf("0x%llx: %-8.*s", + (unsigned long long)insn.vaddr, + CFREE_SLICE_ARG(insn.mnemonic)); + if (insn.operands.len) + driver_printf(" %.*s", CFREE_SLICE_ARG(insn.operands)); + if (insn.annotation.len) + driver_printf(" %.*s", CFREE_SLICE_ARG(insn.annotation)); + driver_printf("\n"); + shown++; + } + cfree_disasm_iter_free(it); +} + +/* ============================================================ * `list file:line` * ============================================================ * Print a context window of source lines centered on `file:line`. @@ -2362,7 +2420,8 @@ static void dbg_cmd_help(void) { "Commands (abbrev. shown):\n" " h, help show this help\n" " q, quit exit (Ctrl-D also works)\n" - " :language c|toy|asm|wasm select language for jit/expr input\n" + " :language c|toy|asm|wasm/wat\n" + " select language for jit/expr input\n" " r, run start fresh execution at entry\n" " c, cont continue after a stop\n" " s, step single-step one instruction\n" @@ -2390,6 +2449,7 @@ static void dbg_cmd_help(void) { " set NAME VALUE write VALUE into NAME\n" " x ADDR [count] examine memory (count bytes, default " "16)\n" + " disasm [ADDR] [count], x/i disassemble at PC or ADDR\n" " list FILE:LINE, l FILE:LINE source listing around FILE:LINE\n" " info b list breakpoints\n" " info reg, info registers dump registers\n" @@ -2679,6 +2739,33 @@ static int dbg_dispatch(DbgState* s, char* line) { dbg_cmd_list(s, loc); return 0; } + if (driver_streq(cmd, "disasm") || driver_streq(cmd, "x/i")) { + char* addr_s; + char* count_s; + uint64_t addr = s->has_stop ? s->last_stop.regs.pc : 0; + uint64_t count = 8; + size_t used; + rest = dbg_take_word(rest, &addr_s); + if (*addr_s) { + used = dbg_parse_uint(addr_s, &addr); + if (!used || addr_s[used] != '\0') { + driver_errf(DBG_TOOL, "bad address '%.*s'", + CFREE_SLICE_ARG(cfree_slice_cstr(addr_s))); + return 0; + } + dbg_take_word(rest, &count_s); + if (*count_s) { + used = dbg_parse_uint(count_s, &count); + if (!used || count_s[used] != '\0') { + driver_errf(DBG_TOOL, "bad count '%.*s'", + CFREE_SLICE_ARG(cfree_slice_cstr(count_s))); + return 0; + } + } + } + dbg_cmd_disasm(s, addr, (size_t)count); + return 0; + } if (driver_streq(cmd, "x") || driver_streq(cmd, "examine")) { char* addr_s; char* count_s; diff --git a/driver/env/uctx_aarch64_macos.c b/driver/env/uctx_aarch64_macos.c @@ -2,12 +2,21 @@ * Register slots use DWARF numbering (x0..x30, sp at index 31). The * mcontext_t exposes Darwin's __darwin_arm_thread_state64. */ +#include <stddef.h> #include <stdint.h> +#include <string.h> #include "env_posix.h" +static mcontext_t dbg_mcontext(const ucontext_t* uc) { + mcontext_t mc; + memcpy(&mc, (const char*)uc + offsetof(ucontext_t, uc_mcontext), sizeof(mc)); + return mc; +} + void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) { - const struct __darwin_arm_thread_state64* ss = &uc->uc_mcontext->__ss; + mcontext_t mc = dbg_mcontext(uc); + const struct __darwin_arm_thread_state64* ss = &mc->__ss; int i; for (i = 0; i < 29; ++i) f->regs[i] = ss->__x[i]; f->regs[29] = (uint64_t)ss->__fp; @@ -18,7 +27,8 @@ void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) { } void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc) { - struct __darwin_arm_thread_state64* ss = &uc->uc_mcontext->__ss; + mcontext_t mc = dbg_mcontext(uc); + struct __darwin_arm_thread_state64* ss = &mc->__ss; int i; for (i = 0; i < 29; ++i) ss->__x[i] = f->regs[i]; ss->__fp = f->regs[29]; diff --git a/test/dbg/cases/repl-help/args b/test/dbg/cases/repl-help/args @@ -0,0 +1,2 @@ +--language +toy diff --git a/test/dbg/cases/repl-help/expected b/test/dbg/cases/repl-help/expected @@ -0,0 +1,36 @@ +cfree dbg — 'h' for help, 'q' to quit +Commands (abbrev. shown): + h, help show this help + q, quit exit (Ctrl-D also works) + :language c|toy|asm|wasm/wat + select language for jit/expr input + r, run start fresh execution at entry + c, cont continue after a stop + s, step single-step one instruction + sl step to next source line (into calls) + n, next step to next source line (over calls) + finish run until current frame returns + jit [LANG|NAME] { ... } compile and append a language snippet + { ... } same as jit { ... } + edit [LANG|NAME], e [...] edit and append a language snippet + expr EXPR | expr { ... } compile and call an expression thunk + EXPR same as expr EXPR + LANG defaults to the selected language + jump ADDR set PC to ADDR (no resume) + bt, backtrace print stack trace with arguments + b LOC set breakpoint at LOC: + 0xADDR | sym[+off] | file.c:line + ignore N COUNT skip the next COUNT hits of bp N + d N, delete N delete breakpoint N + enable N | disable N toggle breakpoint N + p NAME print variable / global + set NAME VALUE write VALUE into NAME + x ADDR [count] examine memory (count bytes, default 16) + disasm [ADDR] [count], x/i disassemble at PC or ADDR + list FILE:LINE, l FILE:LINE source listing around FILE:LINE + info b list breakpoints + info reg, info registers dump registers + info locals list locals at current PC + info args list args at current PC + info functions [PATTERN] list JIT functions matching PATTERN + info variables [PATTERN] list JIT globals matching PATTERN diff --git a/test/dbg/cases/repl-help/stdin b/test/dbg/cases/repl-help/stdin @@ -0,0 +1,2 @@ +h +q diff --git a/test/dbg/cases/toy-breakpoint-hit/args b/test/dbg/cases/toy-breakpoint-hit/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-breakpoint-hit/expected b/test/dbg/cases/toy-breakpoint-hit/expected @@ -0,0 +1,4 @@ +cfree dbg — 'h' for help, 'q' to quit +Breakpoint 1 at 0xADDR (main) +Breakpoint 1 (main) hit at 0xADDR <main> +Program exited with code 42 diff --git a/test/dbg/cases/toy-breakpoint-hit/main.toy b/test/dbg/cases/toy-breakpoint-hit/main.toy @@ -0,0 +1 @@ +fn main(): i32 { return 42 as i32; } diff --git a/test/dbg/cases/toy-breakpoint-hit/stdin b/test/dbg/cases/toy-breakpoint-hit/stdin @@ -0,0 +1,4 @@ +b main +run +cont +q diff --git a/test/dbg/cases/toy-breakpoint-table/args b/test/dbg/cases/toy-breakpoint-table/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-breakpoint-table/expected b/test/dbg/cases/toy-breakpoint-table/expected @@ -0,0 +1,10 @@ +cfree dbg — 'h' for help, 'q' to quit +Breakpoint 1 at 0xADDR (main) +Num Enb Address Skip Max Spec +1 y 0xADDR 0 0 main +Num Enb Address Skip Max Spec +1 n 0xADDR 0 0 main [disarmed] +Breakpoint 1 will skip the next 2 hits +Num Enb Address Skip Max Spec +1 y 0xADDR 2 0 main +No breakpoints. diff --git a/test/dbg/cases/toy-breakpoint-table/main.toy b/test/dbg/cases/toy-breakpoint-table/main.toy @@ -0,0 +1 @@ +fn main(): i32 { return 0 as i32; } diff --git a/test/dbg/cases/toy-breakpoint-table/stdin b/test/dbg/cases/toy-breakpoint-table/stdin @@ -0,0 +1,10 @@ +b main +info b +disable 1 +info b +enable 1 +ignore 1 2 +info b +d 1 +info b +q diff --git a/test/dbg/cases/toy-empty-repl/expected b/test/dbg/cases/toy-empty-repl/expected @@ -1,4 +1,2 @@ cfree dbg — 'h' for help, 'q' to quit -JIT generation 1 (toy) -JIT generation 2 (toy) $1 = 42 (0x2a) diff --git a/test/dbg/cases/toy-error-recovery/xfail b/test/dbg/cases/toy-error-recovery/xfail @@ -0,0 +1 @@ +Toy frontend state is poisoned instead of rolled back after a failed snippet. diff --git a/test/dbg/cases/toy-expr-block/expected b/test/dbg/cases/toy-expr-block/expected @@ -1,3 +1,2 @@ cfree dbg — 'h' for help, 'q' to quit -JIT generation 1 (toy) $1 = 7 (0x7) diff --git a/test/dbg/cases/toy-expr-call/args b/test/dbg/cases/toy-expr-call/args @@ -0,0 +1,2 @@ +--language +toy diff --git a/test/dbg/cases/toy-expr-call/expected b/test/dbg/cases/toy-expr-call/expected @@ -0,0 +1,2 @@ +cfree dbg — 'h' for help, 'q' to quit +$1 = 42 (0x2a) diff --git a/test/dbg/cases/toy-expr-call/stdin b/test/dbg/cases/toy-expr-call/stdin @@ -0,0 +1,3 @@ +jit { fn add(a: i64, b: i64): i64 { return a + b; } } +add(19, 23) +q diff --git a/test/dbg/cases/toy-expr-scalar/args b/test/dbg/cases/toy-expr-scalar/args @@ -0,0 +1,2 @@ +--language +toy diff --git a/test/dbg/cases/toy-expr-scalar/expected b/test/dbg/cases/toy-expr-scalar/expected @@ -0,0 +1,3 @@ +cfree dbg — 'h' for help, 'q' to quit +$1 = 42 (0x2a) +$2 = 77 (0x4d) diff --git a/test/dbg/cases/toy-expr-scalar/stdin b/test/dbg/cases/toy-expr-scalar/stdin @@ -0,0 +1,3 @@ +21 * 2 +(5 + 6) * 7 +q diff --git a/test/dbg/cases/toy-file-line-step/args b/test/dbg/cases/toy-file-line-step/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-file-line-step/expected b/test/dbg/cases/toy-file-line-step/expected @@ -0,0 +1,6 @@ +cfree dbg — 'h' for help, 'q' to quit +Breakpoint 1 at 0xADDR (@CASE@/main.toy:2) +Breakpoint 1 (@CASE@/main.toy:2) hit at 0xADDR <main+0x60> at @CASE@/main.toy:2:16 +Breakpoint hit at 0xADDR <main+0x64> at @CASE@/main.toy:3:20 +Breakpoint hit at 0xADDR <main+0x68> at @CASE@/main.toy:4:10 +Program exited with code 42 diff --git a/test/dbg/cases/toy-file-line-step/main.toy b/test/dbg/cases/toy-file-line-step/main.toy @@ -0,0 +1,5 @@ +fn main(): i32 { + let x: i64 = 10; + let y: i64 = x + 32; + return y as i32; +} diff --git a/test/dbg/cases/toy-file-line-step/stdin b/test/dbg/cases/toy-file-line-step/stdin @@ -0,0 +1,6 @@ +b @CASE@/main.toy:2 +run +step +next +cont +q diff --git a/test/dbg/cases/toy-local-inspection/args b/test/dbg/cases/toy-local-inspection/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-local-inspection/expected b/test/dbg/cases/toy-local-inspection/expected @@ -0,0 +1,9 @@ +cfree dbg — 'h' for help, 'q' to quit +Breakpoint 1 at 0xADDR (@CASE@/main.toy:3) +Breakpoint 1 (@CASE@/main.toy:3) hit at 0xADDR <helper+0x64> at @CASE@/main.toy:3:10 + v = 40 (0x28) + n = 42 (0x2a) +v = 40 (0x28) +n = 42 (0x2a) +n = 7 (0x7) +Program exited with code 7 diff --git a/test/dbg/cases/toy-local-inspection/main.toy b/test/dbg/cases/toy-local-inspection/main.toy @@ -0,0 +1,8 @@ +fn helper(v: i64): i64 { + let n: i64 = v + 2; + return n; +} + +fn main(): i32 { + return helper(40) as i32; +} diff --git a/test/dbg/cases/toy-local-inspection/stdin b/test/dbg/cases/toy-local-inspection/stdin @@ -0,0 +1,10 @@ +b @CASE@/main.toy:3 +run +info args +info locals +p v +p n +set n 7 +p n +cont +q diff --git a/test/dbg/cases/toy-local-inspection/xfail b/test/dbg/cases/toy-local-inspection/xfail @@ -0,0 +1 @@ +Toy does not yet emit usable local/argument DWARF locations for dbg p/set/info. diff --git a/test/dbg/cases/toy-persistent-repl/expected b/test/dbg/cases/toy-persistent-repl/expected @@ -1,12 +1,5 @@ cfree dbg — 'h' for help, 'q' to quit Language: toy -JIT generation 1 (toy) -JIT generation 2 (toy) $1 = 42 (0x2a) -JIT generation 3 (toy) -JIT generation 4 (toy) $2 = 82 (0x52) -JIT generation 5 (toy) -JIT generation 6 (toy) -JIT generation 7 (toy) $3 = 9 (0x9) diff --git a/test/dbg/cases/toy-resume-without-stop/args b/test/dbg/cases/toy-resume-without-stop/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-resume-without-stop/expected b/test/dbg/cases/toy-resume-without-stop/expected @@ -0,0 +1 @@ +cfree dbg — 'h' for help, 'q' to quit diff --git a/test/dbg/cases/toy-resume-without-stop/main.toy b/test/dbg/cases/toy-resume-without-stop/main.toy @@ -0,0 +1 @@ +fn main(): i32 { return 0 as i32; } diff --git a/test/dbg/cases/toy-resume-without-stop/stderr b/test/dbg/cases/toy-resume-without-stop/stderr @@ -0,0 +1,5 @@ +dbg: no program is running; use 'r' to start +dbg: no program is running; use 'r' to start +dbg: no program is running; use 'r' to start +dbg: no program is running; use 'r' to start +dbg: no program is running; use 'r' to start diff --git a/test/dbg/cases/toy-resume-without-stop/stdin b/test/dbg/cases/toy-resume-without-stop/stdin @@ -0,0 +1,6 @@ +cont +step +sl +next +finish +q diff --git a/test/dbg/cases/toy-run-exit/args b/test/dbg/cases/toy-run-exit/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-run-exit/expected b/test/dbg/cases/toy-run-exit/expected @@ -0,0 +1,2 @@ +cfree dbg — 'h' for help, 'q' to quit +Program exited with code 42 diff --git a/test/dbg/cases/toy-run-exit/main.toy b/test/dbg/cases/toy-run-exit/main.toy @@ -0,0 +1 @@ +fn main(): i32 { return 42 as i32; } diff --git a/test/dbg/cases/toy-run-exit/stdin b/test/dbg/cases/toy-run-exit/stdin @@ -0,0 +1,2 @@ +run +q diff --git a/test/dbg/cases/toy-source-list/args b/test/dbg/cases/toy-source-list/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-source-list/expected b/test/dbg/cases/toy-source-list/expected @@ -0,0 +1,8 @@ +cfree dbg — 'h' for help, 'q' to quit + 1 fn helper(v: i64): i64 { + 2 > return v + 2; + 3 } + 4 + 5 fn main(): i32 { + 6 let x: i64 = helper(40); + 7 return x as i32; diff --git a/test/dbg/cases/toy-source-list/main.toy b/test/dbg/cases/toy-source-list/main.toy @@ -0,0 +1,8 @@ +fn helper(v: i64): i64 { + return v + 2; +} + +fn main(): i32 { + let x: i64 = helper(40); + return x as i32; +} diff --git a/test/dbg/cases/toy-source-list/stdin b/test/dbg/cases/toy-source-list/stdin @@ -0,0 +1,2 @@ +list @CASE@/main.toy:2 +q diff --git a/test/dbg/cases/toy-stopped-commands/args b/test/dbg/cases/toy-stopped-commands/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-stopped-commands/expected b/test/dbg/cases/toy-stopped-commands/expected @@ -0,0 +1 @@ +cfree dbg — 'h' for help, 'q' to quit diff --git a/test/dbg/cases/toy-stopped-commands/main.toy b/test/dbg/cases/toy-stopped-commands/main.toy @@ -0,0 +1 @@ +fn main(): i32 { return 0 as i32; } diff --git a/test/dbg/cases/toy-stopped-commands/stderr b/test/dbg/cases/toy-stopped-commands/stderr @@ -0,0 +1,3 @@ +dbg: no program is stopped +dbg: no program is stopped +dbg: no program is stopped diff --git a/test/dbg/cases/toy-stopped-commands/stdin b/test/dbg/cases/toy-stopped-commands/stdin @@ -0,0 +1,4 @@ +bt +info reg +x 0 4 +q diff --git a/test/dbg/cases/toy-stopped-inspection/args b/test/dbg/cases/toy-stopped-inspection/args @@ -0,0 +1,3 @@ +--language +toy +@CASE@/main.toy diff --git a/test/dbg/cases/toy-stopped-inspection/expected b/test/dbg/cases/toy-stopped-inspection/expected @@ -0,0 +1,55 @@ +cfree dbg — 'h' for help, 'q' to quit +Breakpoint 1 at 0xADDR (main) +Breakpoint 1 (main) hit at 0xADDR <main> +#0 0xADDR <main> [no debug info for this frame] +pc 0xADDR +cfa 0xADDR +x0 0xADDR +x1 0xADDR +x2 0xADDR +x3 0xADDR +x4 0xADDR +x5 0xADDR +x6 0xADDR +x7 0xADDR +x8 0xADDR +x9 0xADDR +x10 0xADDR +x11 0xADDR +x12 0xADDR +x13 0xADDR +x14 0xADDR +x15 0xADDR +x16 0xADDR +x17 0xADDR +x18 0xADDR +x19 0xADDR +x20 0xADDR +x21 0xADDR +x22 0xADDR +x23 0xADDR +x24 0xADDR +x25 0xADDR +x26 0xADDR +x27 0xADDR +x28 0xADDR +x29 0xADDR +x30 0xADDR +sp 0xADDR +0xADDR: sub sp, sp, #32 +0xADDR: add x17, sp, #16 +0xADDR: stp x29, x30, [x17] +0xADDR: add x29, sp, #16 +0xADDR: b 0xADDR +0xADDR: nop +0xADDR: nop +0xADDR: nop +0xADDR: sub sp, sp, #32 +0xADDR: add x17, sp, #16 +0xADDR: stp x29, x30, [x17] +0xADDR: add x29, sp, #16 +0xADDR: b 0xADDR +0xADDR: nop +0xADDR: nop +0xADDR: nop +Program exited with code 42 diff --git a/test/dbg/cases/toy-stopped-inspection/main.toy b/test/dbg/cases/toy-stopped-inspection/main.toy @@ -0,0 +1 @@ +fn main(): i32 { return 42 as i32; } diff --git a/test/dbg/cases/toy-stopped-inspection/stderr b/test/dbg/cases/toy-stopped-inspection/stderr @@ -0,0 +1 @@ +dbg: read failed at 0x0 diff --git a/test/dbg/cases/toy-stopped-inspection/stdin b/test/dbg/cases/toy-stopped-inspection/stdin @@ -0,0 +1,9 @@ +b main +run +bt +info reg +disasm +x/i +x 0 4 +cont +q diff --git a/test/dbg/cases/toy-structured-expr/expected b/test/dbg/cases/toy-structured-expr/expected @@ -1,4 +1,2 @@ cfree dbg — 'h' for help, 'q' to quit -JIT generation 1 (toy) -JIT generation 2 (toy) $1 = Point { x: 4, y: 5 } diff --git a/test/dbg/cases/toy-structured-expr/xfail b/test/dbg/cases/toy-structured-expr/xfail @@ -0,0 +1 @@ +Toy expression thunks currently return scalar i64 values only. diff --git a/test/dbg/run.sh b/test/dbg/run.sh @@ -3,9 +3,10 @@ # # Each case lives in test/dbg/cases/<name>/ and may contain: # args one cfree-dbg argument per line; @CASE@ expands to the case dir -# stdin commands fed to the REPL +# stdin commands fed to the REPL; @CASE@ expands to the case dir # expected normalized stdout golden # stderr optional exact stderr golden; absent means stderr must be empty +# xfail optional reason; expected failure until the feature is implemented # # The stdout normalizer removes interactive prompts so goldens focus on # debugger-visible events. It intentionally leaves generation numbers and @@ -40,34 +41,72 @@ trap 'rm -rf "$work_root"' EXIT pass=0 fail=0 +xfail=0 +xpass=0 +skip=0 failures= normalize_stdout() { - sed -e 's/(cfree) //g' -e 's/^ > //' -e 's/^expr > //' "$1" | + sed -e "s|$2|@CASE@|g" \ + -e 's/(cfree) //g' -e 's/^ > //' -e 's/^expr > //' "$1" | + sed -E 's/0x[[:xdigit:]]{8,}/0xADDR/g' | + sed -E 's/[[:space:]]+$//' | sed '/^$/d' } +case_selected() { + [ -n "${DBG_CASE:-}" ] || return 0 + for pat in $(printf '%s' "$DBG_CASE" | tr ',' ' '); do + case "$1" in + $pat) return 0 ;; + esac + done + return 1 +} + for case_dir in "$cases_dir"/*; do [ -d "$case_dir" ] || continue name=$(basename "$case_dir") + if ! case_selected "$name"; then + skip=$((skip + 1)) + continue + fi stdin_file="$case_dir/stdin" expected="$case_dir/expected" expected_stderr="$case_dir/stderr" + xfail_file="$case_dir/xfail" raw_stdout="$work_root/$name.stdout.raw" actual_stdout="$work_root/$name.stdout" raw_stderr="$work_root/$name.stderr" actual_stderr="$work_root/$name.stderr.actual" + case_stdin="$work_root/$name.stdin" + is_xfail=0 + reason= + ok=1 + why= + + if [ -e "$xfail_file" ]; then + is_xfail=1 + reason=$(sed -n '1p' "$xfail_file") + fi if [ ! -e "$stdin_file" ]; then - printf 'FAIL %s (missing stdin)\n' "$name" - fail=$((fail + 1)) - failures="$failures $name" - continue + ok=0 + why="missing stdin" + elif [ ! -e "$expected" ]; then + ok=0 + why="missing expected" fi - if [ ! -e "$expected" ]; then - printf 'FAIL %s (missing expected)\n' "$name" - fail=$((fail + 1)) - failures="$failures $name" + + if [ "$ok" -eq 0 ]; then + if [ "$is_xfail" -eq 1 ] && [ "${DBG_STRICT_XFAIL:-0}" != 1 ]; then + printf 'XFAIL %s (%s)\n' "$name" "$why" + xfail=$((xfail + 1)) + else + printf 'FAIL %s (%s)\n' "$name" "$why" + fail=$((fail + 1)) + failures="$failures $name" + fi continue fi @@ -84,9 +123,10 @@ for case_dir in "$cases_dir"/*; do done < "$args_file" fi - "$CFREE" dbg "$@" < "$stdin_file" > "$raw_stdout" 2> "$raw_stderr" + sed "s|@CASE@|$case_dir|g" "$stdin_file" > "$case_stdin" + "$CFREE" dbg "$@" < "$case_stdin" > "$raw_stdout" 2> "$raw_stderr" rc=$? - normalize_stdout "$raw_stdout" > "$actual_stdout" + normalize_stdout "$raw_stdout" "$case_dir" > "$actual_stdout" if [ -e "$expected_stderr" ]; then cp "$raw_stderr" "$actual_stderr" else @@ -94,47 +134,71 @@ for case_dir in "$cases_dir"/*; do fi if [ "$rc" -ne 0 ]; then - printf 'FAIL %s (cfree dbg exit=%d)\n' "$name" "$rc" - sed 's/^/ stdout| /' "$raw_stdout" - sed 's/^/ stderr| /' "$raw_stderr" - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - if ! diff -u "$expected" "$actual_stdout" >/dev/null 2>&1; then - printf 'FAIL %s (stdout)\n' "$name" - diff -u "$expected" "$actual_stdout" || true - cp "$actual_stdout" "$case_dir/actual" 2>/dev/null || true - fail=$((fail + 1)) - failures="$failures $name" - continue + ok=0 + why="cfree dbg exit=$rc" + elif ! diff -u "$expected" "$actual_stdout" >/dev/null 2>&1; then + ok=0 + why="stdout" + elif [ -e "$expected_stderr" ]; then + if ! diff -u "$expected_stderr" "$actual_stderr" >/dev/null 2>&1; then + ok=0 + why="stderr" + fi + elif [ -s "$raw_stderr" ]; then + ok=0 + why="unexpected stderr" fi - if [ -e "$expected_stderr" ]; then - if ! diff -u "$expected_stderr" "$actual_stderr" >/dev/null 2>&1; then - printf 'FAIL %s (stderr)\n' "$name" - diff -u "$expected_stderr" "$actual_stderr" || true + if [ "$ok" -eq 1 ]; then + if [ "$is_xfail" -eq 1 ]; then + printf 'XPASS %s' "$name" + [ -n "$reason" ] && printf ' (%s)' "$reason" + printf '\n' + xpass=$((xpass + 1)) fail=$((fail + 1)) failures="$failures $name" - continue + else + printf 'PASS %s\n' "$name" + pass=$((pass + 1)) fi - elif [ -s "$raw_stderr" ]; then - printf 'FAIL %s (unexpected stderr)\n' "$name" - sed 's/^/ | /' "$raw_stderr" - fail=$((fail + 1)) - failures="$failures $name" continue fi - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) + if [ "$is_xfail" -eq 1 ] && [ "${DBG_STRICT_XFAIL:-0}" != 1 ]; then + printf 'XFAIL %s (%s' "$name" "$why" + [ -n "$reason" ] && printf ': %s' "$reason" + printf ')\n' + xfail=$((xfail + 1)) + continue + fi + + printf 'FAIL %s (%s)\n' "$name" "$why" + case "$why" in + stdout) + diff -u "$expected" "$actual_stdout" || true + cp "$actual_stdout" "$case_dir/actual" 2>/dev/null || true + ;; + stderr) + diff -u "$expected_stderr" "$actual_stderr" || true + ;; + "unexpected stderr") + sed 's/^/ | /' "$raw_stderr" + ;; + cfree\ dbg\ exit=*) + sed 's/^/ stdout| /' "$raw_stdout" + sed 's/^/ stderr| /' "$raw_stderr" + ;; + esac + fail=$((fail + 1)) + failures="$failures $name" done -total=$((pass + fail)) +total=$((pass + fail + xfail + xpass)) if [ "$fail" -gt 0 ]; then printf '\ndbg: failures:%s\n' "$failures" - printf 'dbg: %d/%d passed\n' "$pass" "$total" + printf 'dbg: %d/%d passed, %d xfailed, %d xpassed, %d skipped\n' \ + "$pass" "$total" "$xfail" "$xpass" "$skip" exit 1 fi -printf '\ndbg: %d/%d passed\n' "$pass" "$total" +printf '\ndbg: %d/%d passed, %d xfailed, %d skipped\n' \ + "$pass" "$total" "$xfail" "$skip"