Skip to content

feat(debugger): CodeTracer time-travelling debugger support#7758

Open
nickysn wants to merge 229 commits intonoir-lang:masterfrom
blocksense-network:nargo_trace
Open

feat(debugger): CodeTracer time-travelling debugger support#7758
nickysn wants to merge 229 commits intonoir-lang:masterfrom
blocksense-network:nargo_trace

Conversation

@nickysn
Copy link
Contributor

@nickysn nickysn commented Mar 20, 2025

Description

This adds support for the CodeTracer time-travelling debugger:

https://github.com/metacraft-labs/codetracer

It also includes several fixes for the nargo debugger.

Problem*

  1. Adds support for CodeTracer (nargo trace command).

  2. Adds the nargo format-trace command, which pretty-prints files, produced by nargo trace.

  3. Resolves:
    Debugger enters both the "then" and the "else" statement of a single "if" #5922

  4. Resolves other bugs in the debugger (nargo debug)

Summary*

This adds a new command nargo trace, which produces an execution trace, compatible with CodeTracer. Also adds another command nargo format-trace, which pretty-prints the nargo trace files.

It also includes several debugger fixes.

Additional Context

This is extracted from the Blocksense Noir branch at:

https://github.com/blocksense-network/noir

By cherry picking only the commits, related to nargo trace and the debugger.

Documentation*

Check one:

  • No documentation needed.
  • Documentation included in this PR.
  • [For Experimental Features] Documentation to be submitted in a separate PR.

PR Checklist*

  • I have tested the changes locally.
  • I have formatted the changes with Prettier and/or cargo fmt on default settings.

smanilov and others added 30 commits March 20, 2025 10:36
This commit introduces `nargo trace`. When invoked, it behaves as if
`nargo debug` is invoked, and then the `next` command is used in the
REPL until the end of the program is reached.

At the moment, just the number of `next` commands is counted and
reported.

The next commit will generate and write a `trace.json` file that will
contain a record of the execution trace.

To test:

```
cd test_programs/plonky2_prove_success/zk_dungeon/
nargo trace --trace-dir /tmp
```

It should output the following:

```
Total tracing steps: 3644
```

Note that the trace-dir is not used at the moment, so it doesn't matter
what you pass as an argument.

(cherry picked from commit 96f3b29)
This scaffolding allows the additions of tests for `nargo trace` in the
same vein as tests are added for other nargo commands.

To check locally what this PR does, checkout the repo and run

`cargo test trace`

It should result in a single test running successfully.

To convince yourself that the test is real, try changing the info
message printed in `tooling/tracer/src/lib.rs:117` and running the test
again.

Note: there is no actual tracing logic yet, but this will be added in
following commits.

(cherry picked from commit 818d433)
Some of the copy-crud related to witnesses is deleted from
`trace_cmd.rs:run`, `trace_program_and_decode`, `trace_program`, and
`trace_circuit`. `trace_circuit` now returns a `TraceArtifact`, which is
saved to a file in `trace_program` and any errors are propagated back.

The test scaffold is updated to look for an `expected_trace.json` in
each test directory and compares it with the file generated by running
`nargo trace`. Also, the test verifies that `nargo trace` prints an info
message that the file has been saved.

(cherry picked from commit 3829579)
This commit has no functional changes.

To verify, run `cargo test trace`, which should still pass (the
test is not touched by this commit).

Changes:
 - `next_into` is renamed to `step_debugger`; a doccomment is added
 - the function is simplified: it now performs less matching
 - `validate_in_progress`, a remnant from the debugger REPL is no longer
   needed, so it's removed
 - `handle_debug_command_result`: ditto
 - the field `context` is renamed to `debug_context`, to reduce
   potential confusion
 - the complexity (matching of the debugger result) is lifted into the
   `trace_circuit` function

Next commit to start adding more logic to `step_debugger`.

(cherry picked from commit d4d4813)
Before this commit, a tracing step was a jump over one opcode.

After this commit, a tracing step is a jump over one line in the source
code.

The test relevant to tracing is updated, so now the expected number of
tracing steps is less (7, instead of 14). For some reason, the test does
not print "warning: no call stack" anymore. I'm not completely sure why,
but this is a wanted improvement, so the expectation for that message is
removed.

There is a potential low-priority bug, involving function calls. A TODO
for it is added in the code.

**Details**

The following functions are no longer private in `DebugContext`:
 * `get_call_stack`
 * `get_source_location_for_opcode_location`

Although this is not ideal, it is an acceptable middle-term solution,
until common logic of the debugger and the tracer is extracted out of
the debugger.

The following functions are added to `DebugContext`, in order to surface
functionality of the contained `debug_artifact`:
 * `get_filepath_for_location`
 * `get_line_for_location`

In `tracer/src/lib.rs`:
 * the initializing logic that checks that the circuit contains opcodes
   is now moved into the `trace_circuit` function, rather than in the
   constructor for `TracingContext`;
 * `debug_context` is renamed to `tracing_context`, which matches the
   type;
 * the error for when there is a breakpoint during tracing is promoted
   to `panic!`, as it should never happen (indicates a serious bug if it
   happens);
 * `last_result` is no longer stored in TracingContext, to keep things
   purer.

**Next steps**

More tests need to be added to explore the error modes. When is there no
call stack? When is there no mapping from opcodes to source locations?
This is not a priority, however, so the first follow-up that is coming
is actually recording information on each step and saving it in the
tracing artifact.

(cherry picked from commit dde0fcd)
This commit imports `metacraft-labs/runtime_tracing` as a dependency and
uses it instead of `TraceArtifact` to construct the tracing record.

For this reason, `tooling/nargo_cli/src/cli/fs/trace.rs` and
`tooling/noirc_artifacts/src/trace.rs` are no longer needed.

`update_record` actually propagates some information to `Tracer`, which
is now written to the output. This can be seen from the fact that
`expected_trace.json` is now non-empty.

To test, run the following:

```
cargo test trace
```

The test still passes, exactly because of the change in
`expected_trace.json`.

**Next steps**

The next steps include recording function calls and variables.

(cherry picked from commit ba7ab37)
This commit just outlines some logic into the
`get_current_line_and_filepath` function, to keep the body of
`step_debugger` fitting on a single screen.

Also, a long TODO comment is replaced with a reference to a filed bug.

There is no functional change: the test still passes:

```
cargo test trace
```

(cherry picked from commit adb4904)
Before this commit, the `expected_trace.json` files had to have the
exact same format as the output of the serializer, which for size
considerations is not pretty printed.

Now, the files are not diff'ed exactly, but read into
serde Value's, which allows the comparison to ignore formatting.

This is illustrated by formatting the one test that currently exists.
The test still passes, even though the actual generated .json looks like
how `expected_trace.json` looked before this commit.

(cherry picked from commit 2fcf540)
The generated trace from this trace illustrates a problem: lines
containing only function calls (e.g. lines 6, 10, and 11) are skipped in
the currently generated trace.

A follow-up PR will address this problem.

This PR also updated 1_mul/expected_trace.json, because otherwise the
test does not pass (I forgot to update it previously).

(cherry picked from commit 19f72ee)
This commit simplifies the testing scaffold (in
`tooling/nargo_cli/build.rs`) because instead of comparing a message
about the number of steps, we now compare the full traces.

It also updates `expected_trace.json` in the test `2_function_calls`,
but the change is not related to code change in this commit. Rather, the
test was broken in the `plonky2` (default) branch, and this PR fixes
that as a drive-by.

(cherry picked from commit 59f6c69)
Changes:
 * a `SourceLocation` struct is introduced to handle a file-line pair in
   a more hygienic way
 * a `DebugStepResult` is introduced, to simplify the interface between
   `trace_circuit` and `step_debugger`

Run the tracing tests to see that they still pass:

```
cargo test trace
```

There is no change in the tests, which demonstrates that there's no
change in the business logic. This is just a structural change.

(cherry picked from commit 94d8f20)
If the next source line for `nargo debug` is a function call, one
debugger step includes it, together with the first line of the called
function. This is just how `nargo debug` works and a fact of life we
choose not to change.

Instead, this commit takes this into account in order to produce a
correct trace that includes as steps both the invocation site, as well as the
first line of the function.

The tests are updated to demonstrate the effect of the bugfix. Note the
new lines that show up in the expected traces now.

As a drive-by, the initial location that is registered now depends on
the implementation of create_unknown, rather than an ad-hoc invalid
location. (See change in expected_trace.json for 1_mul and call to
`tracer.start`.)

Run tests with:

```
cargo test trace
```

Note: the change in `get_current_source_locations` is actually simpler
than it looks: new scopes and indentation are introduced, however, which
trips up the change diff algorithm.

(cherry picked from commit db866a6)
The new test `3_two_files` contains multiple source files, where
`main.nr` calls into `foo.nr` which calls into `bar.nr`. The
`expected_trace.json` demonstrates that the tracer handles this
correctly.

Note the `path_id` fields in the JSON, which say which file the step is
in.

To run and see that the test passes:

```
cargo test trace
```

(cherry picked from commit 3ab7113)
This commit also records function calls and returns from functions,
without passing the actual arguments and values in either direction.
That would be a follow-up.

Details:
 * `get_variables` is now public in `tooling/debugger/src/context.rs`;
   it is needed to extract the stack frame from the debugger;
 * drive-by: `trace_metadata.json` is also stored, because the frontend
   expects that too;
 * `tail_diff_vecs` is a generic utility function to extract the
   difference between two vectors; it has good documentation, so details
   within;
 * a local structure `StackFrame` is introduced, because the `StackFrame` in
   the debugger does not own its data; here, we need to remember the
   previous stack trace (a vector of `StackFrame`'s) in order to discover
   function calls or returns, so we need to own the data;
 * a `stack_trace` field is added to `TracingContext`; see previous point;
 * the new `fn get_stack_trace` performs the conversion mentioned;
 * the old contents of `update_record` are the basis for
   `tail_diff_vecs`; the new contents use that function to perform the
   same operation as before; in addition, the core logic of the PR is
   here: return events are registered for dropped stack frames and call
   events are registered for new stack frames;
 * all tracing tests are updated, to reflect the new traces that are
   generated.

To test:

```
cargo test trace
```

(cherry picked from commit 38bfd20)
(cherry picked from commit 35aee57)
(cherry picked from commit 1978e83)
(cherry picked from commit 8b40415)
This prepares the function for containing more than just extracting the
function name: the next change will add to it the function call
parameters and their values.

(cherry picked from commit edfc04d)
This commit is a first part of a two-part change; the foundation is laid
here to register variables during execution (debugging) of a Noir
program. The second part change will convert all types, not just Field,
as can be seen in the code from this first part.

The tests deleted in this commit will be restored then.

(cherry picked from commit ed050b2)
The previous commit supports only the conversion of values of type Field
to the appropriate trace type.

This commit also adds support for UnsignedInteger, which allows the two
tests deleted in the previous commit to be restored, because now they
pass.

(cherry picked from commit e28d762)
Usage instruction at the top of the script.

(cherry picked from commit 305ecb5)
This newer version makes some changes to the format of the trace, but in
parts that we are not using yet.

(cherry picked from commit f4d642e)
The `variables` in `StackFrame` are now sorted before construction of the
frame. This is an unenforced invariant of the type. The sorting is thus
not needed anymore after the field is accessed (previously, done in `fn
register_variables`).

The new field of `StackFrame` -- `function_param_indexes` -- is a vector
of indexes in this sorted vector (`variables`). It lists which variables
are function parameters.

`stack_trace` is renamed to `stack_frames` for increased symmetry of the
code (it's of type `Vec<StackFrame>`)

Finally, instead of an empty vec[], actual args are passed to
`register_call`, which are built on top of the other changes mentioned.

One question that remains open is whether `register_value` needs to be
called in `convert_params_to_args_vec` or not. This is noted in a
comment and will be addressed at a later point.

(cherry picked from commit 8b93b2d)
Also, this commit moves the logic of what files need to be generated
from the trace to the `tracer_glue` module which keeps the use-site
sparkling clean.

(cherry picked from commit 887e2f2)
Rewrote the tool in Rust instead of bash so it integrates better with
the rest of our system. Also, it now prints arrays on multiple lines.

More information in README.md.

(cherry picked from commit 314a6e3)
When the `format_trace` package was added, the effect on Cargo.lock was
not added to the commit, so this commit fixes that.

Also, it contains some version bumps as a drive-by.

(cherry picked from commit 55980fd)
nickysn and others added 21 commits July 23, 2025 14:14
…lues

**Problem:**
A recent change in the Noir compiler (noir-lang#9579) modified its output
for reference types. It no longer dereferences them to provide the
underlying value in the trace. This caused the tracer to panic because
it was designed to always receive a `PrintableValue` for a
`PrintableType::Reference`.

**Solution:**
This commit adapts the tracer to this new debugger
behavior. When processing a reference, it now acknowledges that the
`PrintableValue` may not be available.

It constructs a `ValueRecord::Reference` with the correct type
information but uses `ValueRecord::None` as a placeholder for the
dereferenced content. This prevents the panic and correctly represents a
reference to an unknown value within the trace.
@socket-security
Copy link

socket-security bot commented Oct 29, 2025

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants