This project is read-only.

Create the output assembly when there are compilation errors

Topics: C# Language Design, General
Dec 4, 2014 at 8:24 AM
Edited Dec 4, 2014 at 8:25 AM
When refactoring a large codebase, you will occasionally get into situations when there will be a few days before you have resolved all errors, and many lines will need to be commented out just in order to make the source code compile so it can be tested.

My idea is to create a compiler switch -errors-as-warnings which would cause an output assembly to be generated regardless of warnings, with all instances of errors replaced with throw new CompilationErrorException(...). Example:
class C {
    public static void Main() {
        blah();
    }
}
could be emitted as if the program was
class C {
    public static void Main() {
        throw new CompilationErrorsException("CS0103", "The name 'blah' does not exist in the current context");
    }
}
I think this feature could potentially alleviate or even eliminate one of the biggest advantages dynamic languages might have over C#.
Dec 4, 2014 at 5:03 PM
Sounds like a problem that would be better solved by breaking down the codebase into smaller modules which can be built and tested individually. I would imagine that any serious refactoring would break enough of the interfaces making even this kind of compiler chicanery insufficient in order to produce partially-working builds and the exact specifications quite tricky to hammer out.
Dec 4, 2014 at 6:02 PM
Yes, sometimes (but not always) it is possible and better to break the codebase up into smaller modules. However, this is not always an option. The two times I would have really wanted the suggested feature in the last 4 months are:
  1. When porting my C# to Javascript compiler from NRefactory to Roslyn (in effect replacing half of the application, but maintaining the test suite). In this case it would not have been possible to split the application into smaller modules, and
  2. When being tasked with upgrading an external component to a new version in my huge LOB application at work. In this case it probably would have been possible to upgrade modules one by one, if there were modules. However, we are stuck with a huge application we have to work with, and it is absolutely not in scope for my project to change that situation.
Situation 2, when it would have been possible to have a more modular design but there is not is likely way more common, but it doesn't change anything, I can't change it.

And I do not imagine the code working well enough for production, but with this feature it would be possible to run through test cases one by one and fix the breakage.

My modus operandi for the time being for this kind of work is to repeatedly hit Alt+Shift+PageDn (ReSharper go to next error), comment out the body of the failing method, add a #warning TODO: Fix and insert a throw new Exception("TODO"). While this workflow is bearable, I would prefer my suggested switch instead.
Dec 4, 2014 at 6:26 PM
Halo_Four wrote:
Sounds like a problem that would be better solved by breaking down the codebase into smaller modules which can be built and tested individually.
While compilers certainly shouldn't ignore most kinds of errors, I would think there may be many situations where it might be nice to have a rule which would allow/ignore "Obsolete" attributes within a member that was itself tagged "Obsolete", and which would make it easy to auto-generate a temporary version of a class which satisfies all the compile-time requirements of its public interface, but would simply have all members throw NotImplemnetedException and additionally would tag all constructors and public members "obsolete". That would make it easy for a partially-finished program which didn't need to actually execute any of the code associated with a class to use such a temporary class as a substitute for a "real" one which was either causing difficulties or wasn't finished yet; the "obsolete" attributes would help ensure that none of the unimplemented members would get actually get called, but being able to ignore use of obsolete members by other obsolete members would make it easier to end up with a partial program that compiles.
Dec 5, 2014 at 10:07 PM
Throwing in a quick side-note, I know of one other language that does a similar thing. GHC (an implementation of the Haskell language) has a flag -fdefer-type-errors that replaces any unsolveable type error with a runtime exception.

So the feature is already in other (very strongly) statically typed languages, so it has some appeal at least.
Dec 5, 2014 at 10:11 PM
Edited Dec 5, 2014 at 10:16 PM
erikkallen wrote:
My idea is to create a compiler switch -errors-as-warnings which would cause an output assembly to be generated regardless of warnings, with all instances of errors replaced with throw new CompilationErrorException(...).
This is already on our radar for after the current product cycle. It isn't quite as simple as that, because we also want to be resilient against errors in declarations, but we want to do essentially what you're suggesting.
Marked as answer by nmgafter on 12/5/2014 at 2:16 PM
Dec 7, 2014 at 3:05 PM
nmgafter wrote:
erikkallen wrote:
My idea is to create a compiler switch -errors-as-warnings which would cause an output assembly to be generated regardless of warnings, with all instances of errors replaced with throw new CompilationErrorException(...).
This is already on our radar for after the current product cycle. It isn't quite as simple as that, because we also want to be resilient against errors in declarations, but we want to do essentially what you're suggesting.
It is interesting conceptually. It seems like such a can of worms from a semantics point of view. There are so many potential ways that the code could result in a compilation error, where is the line drawn as to what the compiler will permit and emit as throwing an exception? Not being able to resolve a local or member by name is one thing, what about trying to call a method with an overload that doesn't exist, or requiring parameters of a type that can't be resolved?

Also, having been involved in the refactoring of large, monolithic and messy applications written in a variety of languages such as Visual Basic 6.0, C# and JavaScript, my opinion is very much that the looser languages make it significantly harder to prove whether or not the refactored code will work, and the refactoring could be introducing additional errors that you would not immediately notice. I look at the compiler enforcing the syntax and contracts being the first wave of tests that have to pass even before you get to unit and functionality tests.

Another thing that scares the Hell out of me is the potential of people just turning this switch on and leaving it on. I can at least assume that it would not be enabled by default.
Dec 7, 2014 at 3:20 PM
Halo_Four wrote:
It is interesting conceptually. It seems like such a can of worms from a semantics point of view. There are so many potential ways that the code could result in a compilation error, where is the line drawn as to what the compiler will permit and emit as throwing an exception? Not being able to resolve a local or member by name is one thing, what about trying to call a method with an overload that doesn't exist, or requiring parameters of a type that can't be resolved?
I don't find this difficult: Compilation error = compilation error exception thrown, no compilation error (eg. warning or no diagnostic at all) = no compilation error exception thrown.

I do understand, though, the point that in order to tolerate errors in declarations (eg. class does not implement interface member) it will be a little more complex. Another question is what should happen with syntax errors; I think they could probably be considered fatal errors that cause no output generation since syntax errors can always be fixed locally.
Also, having been involved in the refactoring of large, monolithic and messy applications written in a variety of languages such as Visual Basic 6.0, C# and JavaScript, my opinion is very much that the looser languages make it significantly harder to prove whether or not the refactored code will work, and the refactoring could be introducing additional errors that you would not immediately notice. I look at the compiler enforcing the syntax and contracts being the first wave of tests that have to pass even before you get to unit and functionality tests.
I agree with you, but I would love to have the option of not being forced into a sequential workflow, especially since the contracts might have to be redefined during the work of making the tests pass.
Another thing that scares the Hell out of me is the potential of people just turning this switch on and leaving it on. I can at least assume that it would not be enabled by default.
Yes, that is a risk. I had the same worries about the dynamic feature, but my fears seem to have been wrong in that case. Any shop that would even consider leaving this switch on probably doesn't produce any quality software anyway, so I don't think it matters too much.