commit 971f78285dc58ebca6711d7cd4f04d8b56c7a5af parent 669ab15f39b2b7a16c9dd7f77b0acfa4fc3ec1a0 Author: Ryan Sepassi <rsepassi@gmail.com> Date: Thu, 28 May 2026 20:26:04 -0700 Advance Toy dbg REPL tests and TODOs Remove the debugger-specific call command now that Toy expressions can call functions. Move Toy REPL smoke coverage into test/dbg, add focused transcript cases, and document the Toy-first transactional frontend plan. Note: test-dbg currently includes intentional red coverage for Toy error recovery and structured expression results. Diffstat:
22 files changed, 358 insertions(+), 109 deletions(-)
diff --git a/doc/DBG_TODO.md b/doc/DBG_TODO.md @@ -0,0 +1,157 @@ +# dbg TODO + +This is the working list for making `cfree dbg` feel like a good systems-code +REPL. The first milestone is Toy because its frontend already has persistent +REPL state and expression/block input kinds. Most debugger and UI work below is +language-neutral; C-specific expression support can follow once the shared +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. +- `src/dbg/` owns the JIT session, worker thread, signal stop/resume loop, + breakpoint patching, guarded memory access, displaced stepping, and + source-level resume modes. +- Toy supports REPL top-level snippets and expression/block thunks. Persistent + Toy declarations already work across `jit` and `expr` commands. +- C can append normal translation-unit snippets, but raw C expressions/blocks + are not implemented as REPL thunks yet. + +## Test Discipline + +- Follow red-green TDD for dbg work where possible: first add or update the + focused `test/dbg` transcript or unit test so it fails for the current + behavior, then implement the smallest change that makes it pass. +- Golden outputs must encode the desired user-visible behavior, not the current + bug or an overly loose approximation. Prefer exact normalized transcripts + over grep-based checks. +- Keep expectations stable and intentional. If a prompt, generation line, + diagnostic, or stop message is noise, normalize it in the harness or change + the product output deliberately rather than baking accidental text into a + golden. +- When moving coverage, preserve the behavioral expectation in the new test + before deleting the old one. The old test should only be removed once the new + location proves the same behavior red/green. + +## Command Direction + +- `expr` is the way to call functions from the REPL. Users write + `twice(value)` or `fn(arg0, arg1)` in the language they are using instead of + learning a debugger-specific call command. +- The old `call SYMBOL ...` command has been removed. Do not reintroduce a + parallel function-call command unless expression evaluation cannot cover a + specific workflow and the limitation is documented first. + +## Toy-First Milestone + +- [x] Keep `test/dbg` as the scripted REPL test home. Cases should drive + `cfree dbg` through stdin and compare normalized transcripts. +- [x] Move the existing Toy REPL smoke coverage out of `test/driver/run.sh`: + land equivalent exact transcript coverage in `test/dbg`, run it red/green, + then delete the duplicated `test/driver/run.sh` checks. +- [ ] Add Toy transcript coverage for: + - [x] empty-session expression evaluation + - [x] persistent top-level `let` + - [x] persistent functions + - [x] record/type declarations across snippets + - [x] function calls through expressions with integer/address-like + arguments + - [x] expression blocks + - [ ] compile error recovery behavior (`toy-error-recovery` red + 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` + - [ ] `p`, `set`, `info locals`, `info args` + - [ ] `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 + - [ ] 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, + arrays/slices, and other structured values (`toy-structured-expr` + red transcript is in place) + - [ ] pretty-print structured expression results using type info instead + of only showing integer/hex scalar output + - [ ] readable diagnostics that keep the REPL usable after bad input + - [ ] better handling for multi-line input and unmatched braces +- [ ] Improve Toy source/debug info: + - [ ] stable synthetic file names for REPL snippets + - [ ] useful `list` output for synthetic snippets + - [ ] local/argument names and values at expected source stops + +## Toy Transactional Frontend State + +- [ ] Make each Toy REPL compile a transaction. A failed snippet must leave the + persistent Toy state exactly as it was before the snippet started, then the + next valid expression/top-level snippet should compile and run normally. +- [ ] Split durable Toy frontend state from per-input parser state. Durable state + should own functions, globals, named types, type ids, and counters that + survive successful snippets. Parser state should own the lexer, current + token, current CG object, locals, scopes, labels, goto targets, current + function return state, diagnostics, and input kind for one compile. +- [ ] Stop storing per-object CG symbol handles as durable Toy symbol identity. + Keep function/global names, types, and attrs as persistent declarations, and + build a per-compile symbol map when replaying previous declarations into + the current object. +- [ ] Stage new functions, globals, and named/type-table entries until the full + snippet succeeds. Lookups during the snippet should see both durable state + and staged declarations; commit appends staged entries, rollback frees + them. +- [ ] Make rollback safe for both ordinary diagnostics and compiler panics from + CG/backend code. Do not rely on copying the parser struct by value; array + growth and panic longjmp paths have already made that unsafe. +- [ ] Preserve the existing object/backend boundary: the compile-session layer + already discards the failed `ObjBuilder`, and dbg only publishes after a + successful compile. The missing transaction is Toy frontend state, not JIT + publication. +- [ ] Consider changing the compile-session error contract so frontend + diagnostic failures return `CFREE_ERR` without an extra fatal + `frontend failed` diagnostic. Internal compiler panics should still use the + panic path. + +## Shared REPL Work + +- [ ] Add line editing and history. +- [ ] Add completion for commands, symbols, locals, files, and breakpoint ids. +- [ ] Repeat the last stepping command on a blank line. +- [ ] 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. +- [ ] 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 + reuse the command engine without scraping human output. +- [x] Remove the old `call` command; expression calls are the REPL path. + +## Robustness And Portability + +- [ ] 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. +- [ ] Add direct tests for displaced AA64 stepping families. +- [ ] Make session teardown explicit enough for tests that stop while the + worker is parked. +- [ ] Decide whether `test-dbg` should self-skip or fail when the compiled + backend/host cannot support a debug session. +- [ ] Bring x64 and rv64 debug sessions up to the same baseline as AA64. + +## C Later + +- [ ] Teach the C frontend `CFREE_FRONTEND_INPUT_REPL_EXPR` and + `CFREE_FRONTEND_INPUT_REPL_BLOCK`. +- [ ] Preserve C declarations across snippets without leaking frontend internals + into the driver. +- [ ] Support C function calls through normal REPL expressions. +- [ ] Infer C expression result types and print typed values. +- [ ] Allow expression thunks to refer to stopped-frame locals where feasible. +- [ ] Add C transcript tests after the Toy harness is stable. diff --git a/driver/dbg.c b/driver/dbg.c @@ -119,7 +119,6 @@ void driver_help_dbg(void) { " EXPR same as expr EXPR\n" " LANG defaults to the selected " "language\n" - " call SYMBOL [INT_OR_ADDR...] call function, returns uint64_t\n" " jump ADDR set PC to ADDR (no resume)\n" " bt, backtrace print stack trace with arguments\n" " b LOC set breakpoint at LOC:\n" @@ -2068,59 +2067,6 @@ out: if (body) driver_free(s->env, body, body_cap); } -static void dbg_cmd_call(DbgState* s, const char* rest) { - char* tmp; - size_t tmp_size; - char* sym; - char* p; - uint64_t args[8]; - uint32_t nargs = 0; - void* entry; - uint64_t ret = 0; - - tmp = dbg_dup(s->env, rest, driver_strlen(rest), &tmp_size); - if (!tmp) { - driver_errf(DBG_TOOL, "out of memory"); - return; - } - p = dbg_take_word(tmp, &sym); - if (!*sym) { - driver_errf(DBG_TOOL, "usage: call SYMBOL [INT_OR_ADDR ...]"); - goto out; - } - entry = cfree_jit_lookup(s->jit, cfree_slice_cstr(sym)); - if (!entry) { - driver_errf(DBG_TOOL, "symbol not found: %.*s", - CFREE_SLICE_ARG(cfree_slice_cstr(sym))); - goto out; - } - while (*p) { - char* a; - uint64_t v; - size_t used; - p = dbg_take_word(p, &a); - if (!*a) break; - if (nargs == 8u) { - driver_errf(DBG_TOOL, "call supports at most 8 arguments"); - goto out; - } - used = dbg_parse_uint(a, &v); - if (!used || a[used] != '\0') { - driver_errf(DBG_TOOL, "expected integer/address argument, got '%.*s'", - CFREE_SLICE_ARG(cfree_slice_cstr(a))); - goto out; - } - args[nargs++] = v; - } - if (dbg_call_u64_entry(s, entry, args, nargs, &ret) == 0) { - driver_printf("= %llu (0x%llx)\n", (unsigned long long)ret, - (unsigned long long)ret); - } - -out: - driver_free(s->env, tmp, tmp_size); -} - /* ============================================================ * `x ADDR [count]` * ============================================================ @@ -2433,7 +2379,6 @@ static void dbg_cmd_help(void) { " EXPR same as expr EXPR\n" " LANG defaults to the selected " "language\n" - " call SYMBOL [INT_OR_ADDR...] call function, returns uint64_t\n" " jump ADDR set PC to ADDR (no resume)\n" " bt, backtrace print stack trace with arguments\n" " b LOC set breakpoint at LOC:\n" @@ -2572,14 +2517,6 @@ static int dbg_dispatch(DbgState* s, char* line) { dbg_cmd_expr(s, rest); return 0; } - if (driver_streq(cmd, "call")) { - if (!s->session) { - driver_errf(DBG_TOOL, "no JIT session"); - return 0; - } - dbg_cmd_call(s, rest); - return 0; - } if (driver_streq(cmd, "bt") || driver_streq(cmd, "backtrace") || driver_streq(cmd, "where")) { dbg_cmd_bt(s); diff --git a/test/dbg/cases/toy-empty-repl/args b/test/dbg/cases/toy-empty-repl/args @@ -0,0 +1,2 @@ +--language +toy diff --git a/test/dbg/cases/toy-empty-repl/expected b/test/dbg/cases/toy-empty-repl/expected @@ -0,0 +1,4 @@ +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-empty-repl/stdin b/test/dbg/cases/toy-empty-repl/stdin @@ -0,0 +1,3 @@ +{ let value: i64 = 41; } +value + 1 +q diff --git a/test/dbg/cases/toy-error-recovery/args b/test/dbg/cases/toy-error-recovery/args @@ -0,0 +1,2 @@ +--language +toy diff --git a/test/dbg/cases/toy-error-recovery/expected b/test/dbg/cases/toy-error-recovery/expected @@ -0,0 +1,2 @@ +cfree dbg — 'h' for help, 'q' to quit +$1 = 42 (0x2a) diff --git a/test/dbg/cases/toy-error-recovery/stderr b/test/dbg/cases/toy-error-recovery/stderr @@ -0,0 +1,2 @@ +<dbg-jit.toy>:2:15: error: expected expression +dbg: jit compile failed diff --git a/test/dbg/cases/toy-error-recovery/stdin b/test/dbg/cases/toy-error-recovery/stdin @@ -0,0 +1,3 @@ +1 + +41 + 1 +q diff --git a/test/dbg/cases/toy-expr-block/args b/test/dbg/cases/toy-expr-block/args @@ -0,0 +1,2 @@ +--language +toy diff --git a/test/dbg/cases/toy-expr-block/expected b/test/dbg/cases/toy-expr-block/expected @@ -0,0 +1,3 @@ +cfree dbg — 'h' for help, 'q' to quit +JIT generation 1 (toy) +$1 = 7 (0x7) diff --git a/test/dbg/cases/toy-expr-block/stdin b/test/dbg/cases/toy-expr-block/stdin @@ -0,0 +1,2 @@ +expr { return 7; } +q diff --git a/test/dbg/cases/toy-persistent-repl/args b/test/dbg/cases/toy-persistent-repl/args @@ -0,0 +1 @@ +@CASE@/main.toy diff --git a/test/dbg/cases/toy-persistent-repl/expected b/test/dbg/cases/toy-persistent-repl/expected @@ -0,0 +1,12 @@ +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-persistent-repl/main.toy b/test/dbg/cases/toy-persistent-repl/main.toy @@ -0,0 +1 @@ +fn main(): i32 { return 0 as i32; } diff --git a/test/dbg/cases/toy-persistent-repl/stdin b/test/dbg/cases/toy-persistent-repl/stdin @@ -0,0 +1,9 @@ +:language toy +jit { let value: i64 = 41; } +value + 1 +jit { fn twice(v: i64): i64 { return v * 2; } } +twice(value) +jit { record Point { x: i64, y: i64, } } +jit { fn sum_point(): i64 { let p: Point = Point { x: 4, y: 5 }; return p.x + p.y; } } +sum_point() +q diff --git a/test/dbg/cases/toy-structured-expr/args b/test/dbg/cases/toy-structured-expr/args @@ -0,0 +1,2 @@ +--language +toy diff --git a/test/dbg/cases/toy-structured-expr/expected b/test/dbg/cases/toy-structured-expr/expected @@ -0,0 +1,4 @@ +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/stdin b/test/dbg/cases/toy-structured-expr/stdin @@ -0,0 +1,3 @@ +jit { record Point { x: i64, y: i64, } } +Point { x: 4, y: 5 } +q diff --git a/test/dbg/run.sh b/test/dbg/run.sh @@ -0,0 +1,140 @@ +#!/bin/sh +# Scripted `cfree dbg` transcript tests. +# +# 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 +# expected normalized stdout golden +# stderr optional exact stderr golden; absent means stderr must be empty +# +# The stdout normalizer removes interactive prompts so goldens focus on +# debugger-visible events. It intentionally leaves generation numbers and +# command output intact. + +set -u + +script_dir=$(cd "$(dirname "$0")" && pwd) +repo_root=$(cd "$script_dir/../.." && pwd) +cases_dir="$script_dir/cases" + +CFREE="${CFREE:-$repo_root/build/cfree}" +export CFREE + +if [ ! -x "$CFREE" ]; then + echo "dbg: cfree binary not found at $CFREE" >&2 + exit 2 +fi + +host_arch=$(uname -m 2>/dev/null || true) +host_os=$(uname -s 2>/dev/null || true) +case "$host_os:$host_arch" in + Darwin:arm64|Darwin:aarch64|Linux:arm64|Linux:aarch64) ;; + *) + printf 'SKIP test-dbg (unsupported host %s/%s)\n' "$host_os" "$host_arch" + exit 0 + ;; +esac + +work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-dbg-test.XXXXXX") +trap 'rm -rf "$work_root"' EXIT + +pass=0 +fail=0 +failures= + +normalize_stdout() { + sed -e 's/(cfree) //g' -e 's/^ > //' -e 's/^expr > //' "$1" | + sed '/^$/d' +} + +for case_dir in "$cases_dir"/*; do + [ -d "$case_dir" ] || continue + name=$(basename "$case_dir") + stdin_file="$case_dir/stdin" + expected="$case_dir/expected" + expected_stderr="$case_dir/stderr" + 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" + + if [ ! -e "$stdin_file" ]; then + printf 'FAIL %s (missing stdin)\n' "$name" + fail=$((fail + 1)) + failures="$failures $name" + continue + fi + if [ ! -e "$expected" ]; then + printf 'FAIL %s (missing expected)\n' "$name" + fail=$((fail + 1)) + failures="$failures $name" + continue + fi + + set -- + args_file="$case_dir/args" + if [ -e "$args_file" ]; then + while IFS= read -r arg || [ -n "$arg" ]; do + [ -n "$arg" ] || continue + case "$arg" in + \#*) continue ;; + esac + arg=$(printf '%s' "$arg" | sed "s|@CASE@|$case_dir|g") + set -- "$@" "$arg" + done < "$args_file" + fi + + "$CFREE" dbg "$@" < "$stdin_file" > "$raw_stdout" 2> "$raw_stderr" + rc=$? + normalize_stdout "$raw_stdout" > "$actual_stdout" + if [ -e "$expected_stderr" ]; then + cp "$raw_stderr" "$actual_stderr" + else + : > "$actual_stderr" + 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 + 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 + fail=$((fail + 1)) + failures="$failures $name" + continue + 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)) +done + +total=$((pass + fail)) +if [ "$fail" -gt 0 ]; then + printf '\ndbg: failures:%s\n' "$failures" + printf 'dbg: %d/%d passed\n' "$pass" "$total" + exit 1 +fi +printf '\ndbg: %d/%d passed\n' "$pass" "$total" diff --git a/test/driver/run.sh b/test/driver/run.sh @@ -574,52 +574,6 @@ else fail=$((fail + 1)) fi -host_arch=$(uname -m) -host_os=$(uname -s) -if { [ "$host_arch" = "arm64" ] || [ "$host_arch" = "aarch64" ]; } && - { [ "$host_os" = "Darwin" ] || [ "$host_os" = "Linux" ]; }; then - printf 'SKIP %s (C frontend owns REPL expression support)\n' \ - "dbg-raw-expression" - - cat > "$work/dbg-toy-main.toy" <<'SRC' -fn main(): i32 { return 0 as i32; } -SRC - if printf ':language toy\njit { let value: i64 = 41; }\nvalue + 1\njit { fn twice(v: i64): i64 { return v * 2; } }\ntwice(value)\njit { record Point { x: i64, y: i64, } }\njit { fn sum_point(): i64 { let p: Point = Point { x: 4, y: 5 }; return p.x + p.y; } }\nsum_point()\nq\n' | - "$CFREE" dbg "$work/dbg-toy-main.toy" \ - > "$work/dbg-toy-repl.out" 2> "$work/dbg-toy-repl.err" && - grep -q '\$1 = 42 (0x2a)' "$work/dbg-toy-repl.out" && - grep -q '\$2 = 82 (0x52)' "$work/dbg-toy-repl.out" && - grep -q '\$3 = 9 (0x9)' "$work/dbg-toy-repl.out"; then - printf 'PASS %s\n' "dbg-toy-repl" - pass=$((pass + 1)) - else - printf 'FAIL %s\n' "dbg-toy-repl" - sed 's/^/ stdout| /' "$work/dbg-toy-repl.out" - sed 's/^/ stderr| /' "$work/dbg-toy-repl.err" - fail=$((fail + 1)) - fi - - if printf '{ let value: i64 = 41; }\nvalue + 1\nq\n' | - "$CFREE" dbg --language toy \ - > "$work/dbg-empty-toy-repl.out" 2> "$work/dbg-empty-toy-repl.err" && - grep -q '\$1 = 42 (0x2a)' "$work/dbg-empty-toy-repl.out"; then - printf 'PASS %s\n' "dbg-empty-toy-repl" - pass=$((pass + 1)) - else - printf 'FAIL %s\n' "dbg-empty-toy-repl" - sed 's/^/ stdout| /' "$work/dbg-empty-toy-repl.out" - sed 's/^/ stderr| /' "$work/dbg-empty-toy-repl.err" - fail=$((fail + 1)) - fi -else - printf 'SKIP %s (unsupported host %s/%s)\n' \ - "dbg-raw-expression" "$host_os" "$host_arch" - printf 'SKIP %s (unsupported host %s/%s)\n' \ - "dbg-toy-repl" "$host_os" "$host_arch" - printf 'SKIP %s (unsupported host %s/%s)\n' \ - "dbg-empty-toy-repl" "$host_os" "$host_arch" -fi - rm -f "$work/check.o" "$work/a.out" if "$CFREE" check "$work/main.c" > "$work/check.out" 2> "$work/check.err"; then if [ ! -e "$work/check.o" ] && [ ! -e "$work/a.out" ]; then diff --git a/test/test.mk b/test/test.mk @@ -42,6 +42,7 @@ TEST_TARGETS = \ test-coff-mingw-import \ test-coff-windows-ucrt \ test-debug \ + test-dbg \ test-driver \ test-driver-ar \ test-driver-cc \ @@ -219,6 +220,9 @@ $(DEBUG_TEST_BIN): test/debug/roundtrip_unit.c $(LIB_OBJS) @mkdir -p $(dir $@) $(CC) $(TEST_HOST_CFLAGS) -Isrc test/debug/roundtrip_unit.c $(LIB_OBJS) -o $@ +test-dbg: bin + @CFREE=$(abspath $(BIN)) sh test/dbg/run.sh + # aa64 ISA descriptor-table unit test (doc/ASM.md phase 2). Covers # every AA64Format the table maps and the alias-precedence invariant # (first-match disasm picks the alias spelling over the canonical