Some thoughts on Edit and Continue and Design-Time Expression Evaluation (Matt Gertz)

The story of Edit and Continue (which I’ll refer to as EnC) is a very long one.  Having been a cornerstone of Visual Basic in the past, it had always been planned for us to ship an Edit and Continue experience with Visual Studio 2002.  But we didn’t.  Why?  Well, it all came down to time.  VB7 (let’s call it that for convenience, though it’s not the official moniker) was a total rewrite from VB6 – not one line of code was reused.  The new code had to target a different editor (owned by the VS Shell team) and a different runtime (owned by the CLR team).  By the time we were at a state where we could even contemplate doing EnC (around late 2000 or thereabouts), we were quite deep into the product cycle and trying to wrap things up.  We did manage to get some very basic EnC going towards the end, but it was very slow and very limited – not shippable at all.  So, with reluctance, we had to pull it, and VS2002 went out without EnC support, and neither we nor customers were very happy with that at all.

So, a few months before VS2002 shipped, those of us on the compiler team (most of us being new recruits to that area) were told in no uncertain terms that EnC *would* be part of the next major release, that being “Whidbey” (VS2005) – no excuses.  So, although we were deep at work on the point release (“Everett”/VS2003), we started to collaborate with all of the relevant product units to put together an EnC experience that would work.  It was a multi-team effort, as we realized that we’d need runtime tooling, debugger tooling, project re-working, and so on – I think that ultimately about a dozen developers were involved from several product units, not to mention their test teams and program managers to make sure it was working the way it should.  The coding on EnC also pre-dated any other Whidbey coding by a substantial amount – we started work on it right at the end of 2001, and it took nearly four years of solid work to complete and test before it was all done.  It is a huge feature which touched a lot of pre-existing code, and therefore involved loads of testing.  The end result, I feel, is highly satisfactory and on par with VB6.  So, what can you do with EnC?

Actually, before I answer that question, I should first answer the question “What is EnC?”  Edit and Continue is the ability to make changes to your code while debugging (“Edit”) and have those changes reflected throughout the rest of the debugging session without having to restart (“Continue”).  It’s a very useful feature for making those last-minute tweaks to your code.  (See http://msdn2.microsoft.com/en-us/library/ba77s56w.aspx for more details.)  EnC is convolved with Design-Time Expression Evaluation (DTEE), which is the ability to run individual methods from the Immediate Window, even while you’re debugging your main code.  The combination of these two makes for a powerful debugging tool. Here’s a very simple example:  imagine you have the following (admittedly contrived) code:

    Sub Main()

        Dim x As Integer

        x = 5

        x = x / 0

        foo()

    End Sub

 

    Sub foo()

        Dim y As Integer

        y = 5

        y = y / 0

    End Sub

 

When you press F5, the code will break with an exception at the line “x = x / 0”, because that’s dividing by zero.  So, you can immediately change the 0 to a (for example) 2 while you’re still debugging, have the change applied, and keep on going by pressing F5 or F10 or whatever, all without restarting your program execution.  That’s EnC.  However, let’s say that you suspect that maybe the subroutine foo() is similarly broken, and you want to make sure that it isn’t before letting your application run any further.  To check this, you don’t press F5 to continue, but instead in the Immediate Window, you type foo() <enter>.  The subroutine foo() executes while you are still stopped at the exception in Main.  That’s DTEE.  Foo() throws an exception just like Main did (because it is also dividing by zero), and you are taken to the appropriate location in code to fix the error.  Pressing F5 will allow Foo() to then complete its run, and then you can press F5 again to continue your run in Main (since it’s been stopped all this time).  We call this ability to be at two breakpoints at once “nested breakpoints,” and it really helps you make changes in bits and pieces of your code without having to restart your whole test scenario.

You don’t need to be running you application in order to test individual methods in the Immediate Window, though.  You can just type foo() in the Immediate Window to start the DTEE, and VS will spin up a debugging session to run it – you can do EnC on that just as before to edit and correct any errors.  (If you can’t see the Immediate Window when you’re not debugging, you can bring it up by choosing “Debug,” then “Windows,” the “Immediate,” or also by typing Control-Alt-“I” if you are using the VB profile.)   I’ll refer to this method of running DTEE as “cold,” as opposed to a DTEE usage which happens as the same time as a debugging session (and is thus “warm”).

In any event, you have to be slightly careful when doing DTEE on a method.  Consider the following code:

    Dim q As Integer

    Sub Main()

        q = 5

        foo()

    End Sub

    Sub foo()

        Dim y As Integer

        y = 5

        y = y / q

    End Sub

 

If you run the application by pressing F5, this will work fine.  However, if you try to run foo() in the Immediate Window “cold,” it’ll throw an exception.  This is because the variable “q” will never be set to 5 (as Main() won’t be called), and so will default to zero, causing a divide-by-zero error in foo().  To complicate things, the Immediate Window will use the same state as any application that is currently being debugged, so if you do a “warm” DTEE call on foo() and the F5 execution of the application has passed the point where “q” got set to 5 in Main, then foo() will work.  You need to understand what the state of your variables is when doing DTEE, and how your method will be affected by that state.  Things that impact this include using globals instead of arguments to the method, objects passed as arguments that may need to be initialized outside of the method, and other “external to the method” set-up work.  Furthermore, the method being called “warm” may change state which will impact the main run — consider the following:

    Dim q As Integer

    Sub Main()

        q = 5

        foo()

        Stop

        Console.WriteLine(q)

    End Sub

    Sub foo()

        q = q + 1

    End Sub

 

If you run the DTEE command “foo()” from the Immediate Window three times when the program execution is stopped at “Stop,” then the Console.WriteLine(q) will print out 9 instead of 6.  It really is using the same value of “q”, and it will therefore impact the rest of the F5 run.

But let’s get back to EnC.  In order for EnC (or DTEE) to function, the program needs to be stopped.  You cannot edit a running program, nor run methods in a running program via DTEE.  And even when you are stopped, not all method changes are continuable.  I was going to list the different kinds of changes that can’t be made in EnC and that require a restart of the application (known as “rude edits”), but the lists on MSDN are pretty good about this — see http://msdn2.microsoft.com/en-us/library/k06a3215.aspx and http://msdn2.microsoft.com/en-us/library/k9x7t598.aspx.  Broadly speaking, additions/deletions of public, generic, or shared methods, public or static variables, method signatures will force you to restart (i.e., anything some other piece of code might also be using), as will certain changes to exception handlers, iteration constraints, and statements which are active higher up the call stack.  The “meat” of the method in which you’re stopped, however, is generally OK to change. 

So, for example, in the following code, if we’ve stopped at the “Stop” line in foo(), then I’m limited to changing the line before and after the call to foo() in Main, the line before “Stop” in foo(), and anything I want in bar() (except its signature).  Anything else will require me to stop my debugging session.  VB helpfully indicates which lines & methods are on the stack by coloring them as grey.

    Dim q As Integer

    Sub Main()

        q = 5

        foo()

        bar()

    Console.WriteLine(q)

    End Sub

    Sub foo()

        q = q + 1

        Stop

    End Sub

    Sub bar()

        q = q + 2

    End Sub

 

Note that, if your current line of execution is the “Stop” in the above code, it’s possible to legally change the “q = 5” line in Main() without requiring a restart, but since your program execution is already past that point, it won’t affect the current debugging session, unless you step out of foo() and either drag the current statement pointer to some point on or before the changed line, or use “Set Next Statement” in the context menu to point to accomplish the same thing.  Changing the “q = q + 2” line (or adding other code) in bar() can also be legal and it will definitely affect the debugging session, since you haven’t gotten to that point in the execution flow yet.

Here’s another example:

    Sub Main()

        Stop

        Dim x As New A

    End Sub

    Class A

        Public a1 As Integer

    End Class

 

If I try removing the “Public a1 as Integer” line from A while debugging, or if I try to add a new public variable to A, then that’s a rude edit since I’ll have changed A’s public interface.  Ditto for protected variables.  However, I can add private variables to A as much as I like, though I can’t delete any existing ones as they may be in use elsewhere by some instances of the class.  Within a method, on the other hand, I can add as many local variables as I like (since they are private to the method), and can delete them if I like, provided in the latter case that I delete all references to them as well.

If, by some chance, you make an edit which requires you to stop & restart, VB will put a grey squiggly line under the affected code to let you know that (complete with tooltip), and will also give you the option of reverting the change if you try to step past the code anyway.

But what if I have the same method running on two different threads, and I change one of them – what happens?  Let’s consider the following code: