From f8b13b254b92e11a1270eeed20e3380c644dab40 Mon Sep 17 00:00:00 2001 From: blokyk Date: Tue, 21 Apr 2026 12:49:00 +0200 Subject: [PATCH] libcmd/repl: print backtraces from outer to innermost frame before this change, the innermost frame was all the way at the top, which means that in very nested code you'd have to scroll though dozens or hundreds of frames before even seeing where you are, because the last lines (which are immediately visible when the command returns) would be the nix code's entry point and other outer frames instead, which is rarely relevant. this CL reverses this order, so that the innermost frames are the last ones printed, and thus are immediately visible. (note that this is already how errors are printed by nix in other contexts anyway, the debugger's :bt is the only thing that prints the trace in "forward" order.) because `DebugState.traces()` use the homegrown `libutil::Generator` instead of an stdlib container, we have to do that reversing ourselves, in this case by just storing every element in an `std::list` (in reverse order) and then traversing it. this is feels kinda dirty, but i don't know any other way, and it's not exactly performance-critical. Change-Id: I9f23e40e57f72a251d65a335b3bba3c3f77b935d --- doc/manual/rl-next/repl-reverse-bt.md | 17 +++++ lix/libcmd/repl.cc | 25 ++++--- .../data/debug_frames.test | 9 +-- .../data/regression_9917.test | 66 +++++++++---------- 4 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 doc/manual/rl-next/repl-reverse-bt.md diff --git a/doc/manual/rl-next/repl-reverse-bt.md b/doc/manual/rl-next/repl-reverse-bt.md new file mode 100644 index 000000000..105e01401 --- /dev/null +++ b/doc/manual/rl-next/repl-reverse-bt.md @@ -0,0 +1,17 @@ +--- +synopsis: "Print REPL backtraces in more convenient order" +issues: [] +cls: [5491] +category: "Improvements" +credits: [blokyk] +--- + +When using the debugger, stack traces printed with the `:bt` command were +previously printed in reverse order compared to most other situations where they +appeared: the current stack frame would be printed at the very top, with the +most outer frame at the bottom, meaning that you'd have to scroll up to get a +sense of where you are. + +With this change, the stack frames are printed such that the most relevant ones +are immediatly visible at the bottom, just like other traces in lix (e.g. +ones caused by errors). diff --git a/lix/libcmd/repl.cc b/lix/libcmd/repl.cc index 3a006a3c3..6af2c16fb 100644 --- a/lix/libcmd/repl.cc +++ b/lix/libcmd/repl.cc @@ -699,17 +699,26 @@ void NixRepl::initDebugBuiltinCommands() addCommand( "backtrace", [](NixRepl & repl, const std::string & _arg) { - auto traces = repl.evaluator.debug->traces(); - for (const auto & [idx, i] : enumerate(traces)) { - std::cout << "\n" << ANSI_BLUE << idx << ANSI_NORMAL << ": "; - showDebugTrace(std::cout, repl.evaluator.positions, *i); + auto tracesGenerator = repl.evaluator.debug->traces(); + + // since we want to print the stack trace in reverse order, + // we have to first traverse all frames and accumulate them + // in a list (in which we store each new trace at the /beginning/) + std::list> reversedTraces; + for (const auto trace : tracesGenerator) { + // because the original traces are indexed from 0 upto N, + // this gives us their original index + auto idx = reversedTraces.size(); + reversedTraces.push_front({idx, trace}); + } + + for (const auto & [traceIdx, trace] : reversedTraces) { + std::cout << "\n" << ANSI_BLUE << traceIdx << ANSI_NORMAL << ": "; + showDebugTrace(std::cout, repl.evaluator.positions, *trace); } return ProcessLineResult::PromptAgain; }, - {.aliases = {"bt"}, - .debugModeOnly = true, - .help = "Show trace stack", - .section = "Debug mode"} + {.aliases = {"bt"}, .debugModeOnly = true, .help = "Show trace stack", .section = "Debug mode"} ); addCommand( diff --git a/tests/functional/repl_characterization/data/debug_frames.test b/tests/functional/repl_characterization/data/debug_frames.test index b97a76f56..fdc603ac2 100644 --- a/tests/functional/repl_characterization/data/debug_frames.test +++ b/tests/functional/repl_characterization/data/debug_frames.test @@ -48,13 +48,14 @@ we can now inspect state nix-repl> :bt - 0: error: undefined variable 'a' + + 1: error: Fake frame for debugging purposes «string»:1:10 1| with {}; a | ^ - 1: error: Fake frame for debugging purposes + 0: error: undefined variable 'a' «string»:1:10 1| with {}; a @@ -92,13 +93,13 @@ leaving the debugger from a toplevel error and entering it again doesn't leave o nix-repl> :bt - 0: error: undefined variable 'b' + 1: error: Fake frame for debugging purposes «string»:1:10 1| with {}; b | ^ - 1: error: Fake frame for debugging purposes + 0: error: undefined variable 'b' «string»:1:10 1| with {}; b diff --git a/tests/functional/repl_characterization/data/regression_9917.test b/tests/functional/repl_characterization/data/regression_9917.test index ec147abd4..b1a88f292 100644 --- a/tests/functional/repl_characterization/data/regression_9917.test +++ b/tests/functional/repl_characterization/data/regression_9917.test @@ -11,37 +11,19 @@ This test ensures that continues don't skip opportunities to enter the debugger. nix-repl> :bt - 0: error: breakpoint reached - $TEST_DATA/regression_9917.nix:3:5 - - 2| a = builtins.trace "before inner break" ( - 3| builtins.break { msg = "hello"; } - | ^ - 4| ); - - 1: while calling a function - $TEST_DATA/regression_9917.nix:3:5 - - 2| a = builtins.trace "before inner break" ( - 3| builtins.break { msg = "hello"; } - | ^ - 4| ); - - 2: while calling a function - $TEST_DATA/regression_9917.nix:2:7 + 6: while evaluating the file '$TEST_DATA/regression_9917.nix': + $TEST_DATA/regression_9917.nix:1:1 1| let + | ^ 2| a = builtins.trace "before inner break" ( - | ^ - 3| builtins.break { msg = "hello"; } - 3: while calling a function - $TEST_DATA/regression_9917.nix:6:5 + 5: while evaluating a 'let' expression + $TEST_DATA/regression_9917.nix:1:1 - 5| b = builtins.trace "before outer break" ( - 6| builtins.break a - | ^ - 7| ); + 1| let + | ^ + 2| a = builtins.trace "before inner break" ( 4: while calling a function $TEST_DATA/regression_9917.nix:5:7 @@ -51,19 +33,37 @@ This test ensures that continues don't skip opportunities to enter the debugger. | ^ 6| builtins.break a - 5: while evaluating a 'let' expression - $TEST_DATA/regression_9917.nix:1:1 + 3: while calling a function + $TEST_DATA/regression_9917.nix:6:5 + + 5| b = builtins.trace "before outer break" ( + 6| builtins.break a + | ^ + 7| ); + + 2: while calling a function + $TEST_DATA/regression_9917.nix:2:7 1| let - | ^ 2| a = builtins.trace "before inner break" ( + | ^ + 3| builtins.break { msg = "hello"; } - 6: while evaluating the file '$TEST_DATA/regression_9917.nix': - $TEST_DATA/regression_9917.nix:1:1 + 1: while calling a function + $TEST_DATA/regression_9917.nix:3:5 - 1| let - | ^ 2| a = builtins.trace "before inner break" ( + 3| builtins.break { msg = "hello"; } + | ^ + 4| ); + + 0: error: breakpoint reached + $TEST_DATA/regression_9917.nix:3:5 + + 2| a = builtins.trace "before inner break" ( + 3| builtins.break { msg = "hello"; } + | ^ + 4| ); nix-repl> :c