Something we in the C# Compiler team have been discussing recently is the issue of including "breaking changes" in our code.  This is a big deal, especially for a compiler, which people expect to be pretty rock-solid.  There are a lot of issues here, most of which remain unresolved.

The first issue is the question of what, exactly, a breaking change is.  One could probably make the case that any code change, no matter how minor, could be a "breaking" change because someone could be relying on that particular functionality.  Thus, we're required to make a few assumptions about our users.  A good starting point is to assume that no one relies on crashes of any kind.  Any time we see "Internal Compiler Error" we know that fixing the issue is the right thing to do—a code change that causes the compiler not to crash where it crashed before is not a breaking change.  All right, that one's a no-brainer, and while that's an important class of errors, it hardly covers the majority of issues users might encounter, so let's break down code changes further into two major categories, excluding crashing issues:  there is code that did not compile under the previous version, and code that did compile under the previous version.

If the code didn't compile in the first place, there are two changes we can make.  One is to change the error messages that are printed out, probably with the intention of making them clearer or more relevant to the problem at hand.  It seems reasonable to assume that people wouldn't rely on code not compiling in a certain way—the code didn't compile before, and it still doesn't compile.  This class of errors does not contain breaking changes.  The other possibility is that the code that didn't compile is now accepted.  This also isn't likely to break anyone.  If we add a new feature that reasonably accepts code that would have been illegal before, people aren't likely to complain that we are accepting programs they were expecting to fail (I can't imagine hearing a complaint that class Foo<T> { ... } is accepted by the Whidbey compiler, since it's not valid code in Everett!).  We also don't consider these breaking changes.

All right, so we've determined that it's okay to change things about code that doesn't compile anyway.  I'll take the category of code that did compile and further divide it into three subcategories:  first, changes in warnings generated; second, changes in actual code generated; and third, changes that raise new errors.  Changing the warnings is generally considered acceptable, and is often a prelude to more serious changes.  For example, it is common practice to add a warning on a newly-deprecated construct.  These are not breaking changes.

The next two categories are a bit more serious, and the astute reader will probably think that this is the interesting part.  First, if the generated code is different, that is likely to be a problem for some people who depended on an old behavior.  For example, in Whidbey, we fixed an issue with enum types and integer math, where an enum whose underlying type was byte could be subtracted from another enum of the same type, assigned to an int, and the result would be -1.  Now, if you were to do the same math with the underlying byte type, you would end up with 255, because the byte arithmetic would overflow (byte is unsigned, and operations are unchecked by default).  We're giving the right answer here, but it's still a breaking change because we had real customers who relied on the old behavior, even though it was incorrect.

Finally, let's look at the case where code that was accepted simply fails on a new version.  These are the clearest breaking changes; anyone who had the (previously valid) code would be broken by this sort of change.  Take, for example, our handling of [Conditional(DEBUG)] attribute.  In Everett, we allowed methods marked with [Conditional(DEBUG)] to have out parameters, but in Whidbey, it was disallowed because it cannot be guaranteed that the parameter will be assigned in a release build.  However, this is also a real customer scenario where we broke someone who had [Conditional(DEBUG)] methods that had out parameters.  What seemed to be a sensible design revision caused some customers a lot of trouble.

All this thought has been part of a long discussion among some of us on the C# team on how, exactly, to deal with breaking changes.  This is a long process, starting with devs considering if they might be adding breaking changes, going through QA noticing breaking changes involved in bug fixes, and finally moving on to PMs and other customer-focused team members educating users about what changes have been made.  It's no single person's responsibility, which means that it has not always gotten as much attention as it should.

What's the right solution?  I'll let you know when we come up with it, but for now, we'll be encouraging devs to think about users they may be breaking when they make checkins, asking QA to consider each bug they sign off on carefully, and getting everyone to keep their PMs informed about these changes as soon as they are made and discovered.  That way, we'll have an easier time knowing and documenting what changes exactly may adversely affect our users between versions, and help them transition more smoothly from each version to the next.