Skip to content

feat: add transaction DAP debugging support#41

Merged
bitwalker merged 15 commits intonextfrom
pr/enable-tx-debugging-clean
Mar 26, 2026
Merged

feat: add transaction DAP debugging support#41
bitwalker merged 15 commits intonextfrom
pr/enable-tx-debugging-clean

Conversation

@djolertrk
Copy link
Copy Markdown
Collaborator

No description provided.

Copy link
Copy Markdown
Collaborator

@bitwalker bitwalker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! This is not a thorough review since this is still a draft, but wanted to at least provide some initial feedback

@@ -0,0 +1,26 @@
[package]
name = "dap"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's name this miden-debug-dap

[package]
name = "dap"
version = "0.1.0"
edition = "2021"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use 2024 edition

categories = ["development-tools::debugging"]

[lib]
doctest = false
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason for this? I don't inherently have an issue with it, but like to know why we we use any non-default configurations.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed. I used it locally. Will drop it.

fake = { version = "2.*", features = ["derive"], optional = true }
rand = { version = "0.*", optional = true }

[features]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally always put [features] above [dependencies], since it makes it easier to reason about the latter

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we should convert the repo into a workspace now, we really have a couple of different crates:

  • miden-debug-dap - the DAP server component
  • miden-debug-engine, (currently the [lib] target of miden-debug) - the execution engine component and core data types
  • miden-debug, (the current [bin] target) - the CLI driver and terminal user interface component

The value in breaking out miden-debug-engine is that all of the TUI/CLI dependencies can be avoided in downstream dependents (e.g. the compiler, miden-testing).

We may also end up with more crates in the future, but that's unclear. Using a workspace we can share dependencies and the release process stays simple, so I prefer that route when we have multiple crates in a single repo.

src/ui/state.rs Outdated
Transaction,
/// Debugging remotely via a DAP server connection.
#[cfg(feature = "dap")]
RemoteDap,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just call this Remote. We can also remove the feature-gate, since it will simplify any code that matches on DebugMode (we can simply return an error if it is used without DAP support enabled)

src/ui/state.rs Outdated
return Err(Report::msg("reload is not supported in transaction debug mode"));
}
#[cfg(feature = "dap")]
if self.debug_mode == DebugMode::RemoteDap {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably can consider removing the current reload command, and instead support a remote DAP server telling us to reload (providing the new artifact and where to resume to).

Not something we need to do now, but trying to figure out how we can support edit-and-continue style workflows in IDEs which are debugging code via miden-debug

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with that direction. For now I kept reload as a local-only command and made the remote case fail explicitly, because we don’t yet have a protocol for the server to push a replacement artifact plus resume location/state back to the client.
I think the right long-term shape is what you describe: in remote mode, reload should be driven by the DAP server/debuggee side rather than by the TUI client trying to reconstruct execution locally. I’d prefer to keep that as a follow-up once we define the remote reload/edit-and-continue flow more concretely.

I will add a TODO here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added reload as well. Thanks for the suggestion!

src/ui/state.rs Outdated

#[cfg(feature = "dap")]
if self.debug_mode == DebugMode::RemoteDap {
return Err("memory reads are not supported in DAP remote debug mode".into());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a brutal limitation IMO - why is this something we cannot support?

My mental model for all this is as follows:

  • Someone wants to debug a transaction in miden-client, so they run miden-client with something like miden-client ... --start-debug-server "127.0.0.1:4000". When the client goes to execute the transaction, the miden-debug executor is instantiated in DAP server mode, and starts listening for DAP clients on the configured interface and port
  • Either from my IDE (using an extension built on top of miden-debug's internals), or using the CLI via something like miden-debug --remote 127.0.0.1:4000, we use miden-debug in DAP client mode to connect to the DAP server.
  • The miden-debug DAP client is simply relaying commands to the miden-debug DAP server, and receiving whatever data is necessary to render the UI (or answer queries) over the wire.
  • EIther the DAP client exits (and the server can then execute to completion normally), or the DAP server exits (because the transaction has finished execution, or it was killed)

So both the client and the server can perform memory reads, it is just that the server handles reading the necessary bytes from its state, which the client expects to be sent over the wire, and the decoding of the value (and rendering) is handled client-side.

That said, this is just what I had sketched out in my mind, the actual implementation likely differs. Could you elaborate on your overall vision for how these pieces fit together, and how we'll support some of these important debugging flows?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, and this was just an implementation gap, not a fundamental limitation. I’ve removed that restriction in this branch.

In remote mode, memory reads are now proxied to the DAP server/debuggee side: the remote client sends a read request over DAP, the server evaluates it against its current processor state, and returns the rendered result. So the client no longer tries to read remote memory from local state.

More broadly, my intended model is very close to what you described:

  • miden-client starts transaction execution with a miden-debug DAP-backed executor on the debuggee side
  • miden-debug --remote ... acts as a DAP client/TUI
  • the server/debuggee owns the real execution state
  • the client relays stepping/continue/read-style requests over DAP and refreshes its UI from server responses

The main simplification in the current implementation is that after a stop we still refresh state by querying the server (stack_trace, variables, evaluate("__miden_state")) rather than having the server push a richer state update proactively. I think push/event-driven updates would be a good follow-up, but the overall direction is the same: the remote client is a UI over server-owned execution state, not an independent executor.

// DAP CLIENT MODE
// ================================================================================================

#[cfg(feature = "dap")]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should split up the State internals into two structs, one for normal/server mode, and one for client mode? The requirements are quite different for each, and the client in particular isn't actually responsible for maintaining much state (as the DAP server is the one holding it). Instead, the DAP client relays all commands/requests to the DAP server, and then handles the details of deserializing the responses and updating the UI.

Both "normal" execution and DAP server mode execution are essentially the same from a state perspective, so those can be combined I think.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree!

src/ui/state.rs Outdated
}

/// Refresh the executor state from the DAP server after a step command.
pub fn refresh_from_dap(&mut self) -> Result<(), Report> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rely on the DAP server to tell us about changes rather than querying for them? That would likely be a lot more efficient, but perhaps the protocol doesn't work that way.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, yes, but we can workaround that -- good catch: 1c1f636

@djolertrk djolertrk force-pushed the pr/enable-tx-debugging-clean branch from a0152f1 to 3ad5394 Compare March 19, 2026 11:16
@djolertrk djolertrk marked this pull request as ready for review March 19, 2026 11:17
@djolertrk djolertrk force-pushed the pr/enable-tx-debugging-clean branch 3 times, most recently from 40d48e1 to 7814f4d Compare March 19, 2026 17:05
@bitwalker
Copy link
Copy Markdown
Collaborator

I've found a few issues while reviewing this tonight, but it'll take me a bit to write up the details - I'll try and follow up tonight with those, but I may not be able to until tomorrow after the usual compiler sync - just wanted to give you a heads up. But here's the gist:

  • The DAP executor doesn't return a real ExecutionOutput, so while a transaction can be debugged, it can't be correctly completed, because the adapter returns default values for the advice provider, memory, and the transcript, and the transaction executor consumes the advice provider to reconstruct the transaction outputs.
  • read_memory_at_current_state appears to be using the old endianness layout, e.g. it uses (to|from)_be_bytes, I'm not sure if there are other places where this assumption is still present.
  • It appears that user-defined breakpoints are not synced to the DAP server, so they appear locally but do not actually affect execution
  • It looks like stack traces are collapsed to a single frame when being sent back to the client

@djolertrk
Copy link
Copy Markdown
Collaborator Author

djolertrk commented Mar 23, 2026

The DAP executor doesn't return a real ExecutionOutput, so while a transaction can be debugged, it can't be correctly completed, because the adapter returns default values for the advice provider, memory, and the transcript, and the transaction executor consumes the advice provider to reconstruct the transaction outputs.

Yes, I have left this on purpose (because I realized that the API is not public in the FastProcessor), but it is a leftover. Will fix it.

Also, I will check the rest. Thanks @bitwalker!

@djolertrk
Copy link
Copy Markdown
Collaborator Author

The latest change needs 0xMiden/miden-vm#2901

@bitwalker
Copy link
Copy Markdown
Collaborator

@djolertrk I've merged the miden-vm PR, so this PR will be held up until the next patch release of the VM - in the meantime, we can pin against the git ref for development as needed.

@djolertrk djolertrk force-pushed the pr/enable-tx-debugging-clean branch from 7754da4 to f69f8b6 Compare March 26, 2026 18:49
@djolertrk
Copy link
Copy Markdown
Collaborator Author

@bitwalker This is rebased and aligned with the new ecosystem.

@bitwalker bitwalker merged commit 047592e into next Mar 26, 2026
7 checks passed
@bitwalker bitwalker deleted the pr/enable-tx-debugging-clean branch March 26, 2026 19:25
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.

2 participants