dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.31k stars 1.59k forks source link

Breakpoints don't work on wrapped variable assignments #56932

Open DanTup opened 1 month ago

DanTup commented 1 month ago

A change was recently made to prevent breakpoints from jumping to different lines than the one where they were added if the location was invalid (https://github.com/dart-lang/sdk/issues/56820). While I think the idea is right, it seems to have impacted some kinds of code I hadn't considered and I'm not sure it's a good experience.

Consider this code:

image

I wanted to step into ElementLocator.locate so I put a breakpoint on line 78 expecting it to be the correct one (because I wanted to stop on this statement). However, line 78 is not a valid location to stop, so now we don't pause at all and it feels like a bug.

I'm not sure I could define rules that would fix this without reverting the change for https://github.com/dart-lang/sdk/issues/56820, but perhaps someone more familiar with the internals could.

@bkonyi @derekxu16

dart-github-bot commented 1 month ago

Summary: Breakpoints are not working on variable assignments wrapped in parentheses, preventing stepping into functions called within the assignment. This issue arises from a recent change intended to improve breakpoint behavior, but it seems to have unintended consequences for certain code patterns.

a-siva commented 1 month ago

@derekxu16 will follow up with @DanTup on two options

derekxu16 commented 1 month ago

The debugger currently doesn't know the token range covered by any particular statement, and it would be a significant undertaking to support that, so it's not something we can promise in the foreseeable future. As Siva mentioned, our only options for now are to: 1) keep enforcing that breakpoints can only be resolved on the same line where they were requested (it would be nice to show users which lines contain debuggable code, but I don’t think it’s possible to show that when there’s no active debug session, right?), 2) revert the change and allow breakpoints to move any number of lines, or 3) enforce that breakpoints can only move up to some fixed number of lines. Please let me know what you think.

DanTup commented 1 month ago

we have VS code use the information that the VM provides about lines that are breakable and only allow the user to set breakpoints at those locations.

This would only work when there is a debug session, but breakpoints are often set before a debug session is starting. It might be useful to use this information (just so setting breakpoints when a session is active is better), but it probably doesn't solve this problem well enough.

or 3) enforce that breakpoints can only move up to some fixed number of lines

I think this might be the best experience, though I'm not sure that number should be. I feel like jumping one line is probably reasonable. Jumping 15 lines is probably not. 2? 3? I'm less sure. Maybe we should start with 1 though and see what the feedback is like. I think it will be better than both the old and the new/current behaviour.

WDYT?

a-siva commented 1 month ago

@DanTup jumping one line might sound reasonable but I am worried we may encounter scenarios where it might work better if we had picked two lines or three lines.

I see two scenarios here

  1. The IDE is connected to a debug session, in this case querying the VM to figure out locations where breakpoints can be set and acting accordingly seems like a good experience (maybe even having a gutter with an indication that states where breakpoints can be set)
  2. The IDE is not connected to a debug session, in this case it is harder as there is no information to validate where breakpoints can be set, I see two possible options
    • we set breakpoints at lines that the user specifies and when the user connects to a debug session we try setting the breakpoints and just report back to the user about all the breakpoints that could not be set and have them adjust the lines
    • the IDE tool is made smarter to try and adjust all the breakpoints that could not be set when a debug session is created by retrying at a different location using a policy (maybe +1 line or +2 line or scan until a semi colon is seen and keep trying +n until that line is reached)
DanTup commented 1 month ago

we set breakpoints at lines that the user specifies and when the user connects to a debug session we try setting the breakpoints and just report back to the user about all the breakpoints that could not be set and have them adjust the lines

We could certainly print some output whenever we fail to set a breakpoint in the debug console. Adding the reason as a tooltip to the reason mostly does the same thing, although if it's a short-lived script, they might never be able to hover, so having both could help.

I don't think that addresses the main issue above - it could be quite annoying to have to remember every time you want to set a breakpoint on an assignment like that (particularly if you don't see the text until after the breakpoint is missed, or you're debugging a CLI script that has no exited and you need to run again).

the IDE tool is made smarter to try and adjust all the breakpoints that could not be set when a debug session is created by retrying at a different location using a policy (maybe +1 line or +2 line or scan until a semi colon is seen and keep trying +n until that line is reached)

In the debug adapter (which is the only place we can execute code here, VS Code talks directly to the debug adapter) we don't have easy access to the source code, so I doubt we could do this reliably (probably we could get the source from the VM, but at that point the VM could probably do this itself better/faster?).

jumping one line might sound reasonable but I am worried we may encounter scenarios where it might work better if we had picked two lines or three lines.

It's definitely possible, but I think a variable assignment over 1 line is the only case I can come up with so far. That said, I also feel there's something inconsistent here.. why is the breakpoint on line 6 valid, but the one on line 8 should be 9? If I can stop at 6 I feel like I should be able to stop at 8?

image

derekxu16 commented 3 weeks ago

I also feel there's something inconsistent here.. why is the breakpoint on line 6 valid, but the one on line 8 should be 9? If I can stop at 6 I feel like I should be able to stop at 8?

I just looked into this. For the assignment on line 6, we mark the = on line 6 as a location where breakpoints can be set, because we know that the RHS of the assignment does not already contain any locations where breakpoints can be set, because it is a constant. This is checked by the following code:

https://github.com/dart-lang/sdk/blob/53bad058b156f1a03150a7edc9c51f54f0a03a7e/runtime/vm/compiler/frontend/kernel_to_il.cc#L2274

For the assignment on line 8, we know that the RHS of the assignment is a call, and calls are already locations where breakpoints can be set, so we do not mark the = as a location where breakpoints can be set.

DanTup commented 3 weeks ago

@derekxu16 what's the reason for the difference? (why wasn't it just set on the RHS?)

Are you able to add synthetic locations to break at? Would it be possible for every assignment like this to just have a stop point at the start of the line?

derekxu16 commented 3 weeks ago

@derekxu16 what's the reason for the difference? (why wasn't it just set on the RHS?)

Are you able to add synthetic locations to break at?

Yes, we are able to synthetically mark locations as "breakable". That's what we're doing to the = on line 6. I guess the original writers of this code just thought that it made more sense for the breakpoint end up on the = than on the constant.

Would it be possible for every assignment like this to just have a stop point at the start of the line?

That is possible, but I would personally like it more if the = of every assignment was made "breakable". Let me check with Siva to see if we need to have a meeting to come to a consensus on this.

a-siva commented 3 weeks ago

That is possible, but I would personally like it more if the = of every assignment was made "breakable". Let me check with Siva to see if we need to have a meeting to come to a consensus on this.

A statement like 'int j = foo();' would already have a breakable point at the start of foo correct, in this case an additional breakable at the '=' seems redundant.

DanTup commented 3 weeks ago

That's true, but I don't think if my debugger stopped at the start of the statement and then again on foo() I would be surprised. I feel like that might be the smaller drawback of all the possible solutions here?

derekxu16 commented 3 weeks ago

@DanTup, do you have a strong preference for having the synthetic "breakable" positions be at the start of lines instead of on =?

DanTup commented 3 weeks ago

Not strong, no :)

It is more like where I'd expect a break (because a "line breakpoint on line 10" feels to me like a "stop before executing the statement on line 10"), but I don't think it makes a material difference? (edit: except for maybe if you have pretty weird formatting?)

FMorschel commented 3 weeks ago

Adding the synthetic "breakable" positions at the start of lines would also have a side effect:

void main() {
  ClassA().m();
}

class ClassA {
  int get myInt => 0;

  void m() {
    // code here...
    myInt; // Breakpoint here
    // code here...
  }
}

Today if you place a breakpoint there it doesn't stop. With this change, it would.


I'm more inclined to this option than the "before =" one.

derekxu16 commented 2 weeks ago

Today if you place a breakpoint there it doesn't stop. With this change, it would.

Which version of Dart were you testing on? I just tried this on Dart 3.5.1 and the program did pause on myInt when a breakpoint was set there beforehand.

FMorschel commented 2 weeks ago

Which version of Dart were you testing on?

Sorry! This specific code was not tested. I saw a behaviour like what I mentioned while working on a CL near the time of my comment. I'll see if I can remember which one and create a repro for another issue if that still happens. Thanks for the heads up!


But my position for the placement of the breakpoint stands and this is another example of code that would be familiar to this.

derekxu16 commented 2 weeks ago

Sorry! This specific code was not tested. I saw a behaviour like what I mentioned while working on a CL near the time of my comment. I'll see if I can remember which one and create a repro for another issue if that still happens. Thanks for the heads up!

No problem. Thanks!

But my position for the placement of the breakpoint stands and this is another example of code that would be familiar to this.

The discussion about whether or not to mark = as synthetically "breakable" was specifically related to assignment statements, to address the problem reported in this issue's initial description. If there are other types of lines where breakpoints cannot be set, it should mean that there's no debuggable code on those lines (or there's a bug in the debugger), meaning that it's likely pointless to pause on those lines. We'll have to look at examples of those types of lines and decide whether or not they warrant any tokens in them to be synthetically marked as "breakable".