1/ The C# development team, for V1, consisted of 5 individual engineers, a lead, and a couple of PMs. 4 of the engineers worked on implementing the compiler, and the last engineer worked on the Visual Studio integration (e.g., IntelliSense).
2/ The C# compiler was written entirely in C++ as was much of Visual Studio at the time. Everyone on the team wanted to implement the compiler in C#, but as I mentioned previously, expediency was key and when we started, we needed to bootstrap. That was done in C++, so it stuck.
3/ The compiler was largely implemented as a batch compiler. We had a no-release heap, which was extremely efficient for straight-through compilation and totally useless for interactive scenarios. Therefore, the compiler and what we call the language service were implemented
4/ separately. They did share the parse tree, but that was essentially it. Binding happened entirely differently. This may not sound ideal. It wasn't. However, it did accelerate the compiler implementation. How? In interactive scenarios the language service must be extremely
5/ error tolerant. It needs to provide completion lists with a high degree of accuracy even in the face of what the compiler would traditionally consider catastrophic errors. Simply by definition, there will be syntax errors when the user is typing.
6/ The C# compiler had separate phases, and if there were errors in those phases, it could choose not to proceed to the next phase (e.g., IL gen was a phase that was never executed if parsing and binding didn't happen without errors).
7/ This architectural choice was made, in part, due to the need to deliver a compiler as quickly as possible. It meant that any time a language feature was added or changed, we needed to implement it in both the compiler codebase and the language service.
8/ These components also had different goals, the compiler needed to be 100% faithful to the language, while the language service needed to be error tolerant and fast. This led to subtle and hard to track down differences between the interactive and build-time experiences.
9/ Sometimes these were desirable, like, we might choose to allow goto definition on an inaccessible member because you want to navigate to it and change it from private to public. After all, if you hit F12 on it, that'd probably be your goal right?
10/ But other times, it meant we didn't show an item in a completion list when we should. I could go into a lot of depth on the tradeoffs here, and may later, but the long and short is that we made a very explicit choice up front to prioritize the design of the language,
11/ and the stability, accuracy, throughput, and delivery of the compiler, over code-sharing for the interactive experience. I believe this was the right trade-off at the time, but it led to some significant pain for subsequent versions that I'll talk about in the future :-)
• • •
Missing some Tweet in this thread? You can try to
force a refresh
1/ It's probably not the first thing you think of, but when we started .NET (COM+) in the late 90s, C# didn't exist yet. We were working on it at the same time as the CLR and the framework. So, you might wonder, what language was being used to generate IL and write the BCL?
2/ The answer is a language that we called SMC that Peter Kukol wrote the compiler for. Peter is a flat out amazing engineer and wrote the core parts of the compiler in just a few days. This unblocked the framework team, allowed vetting the runtime and interpreter, etc.
3/ SMC was a trimmed down C++ variant and the compiler was written in itself (i.e. SMC). It didn't support things like destructors, multiple inheritance, virtual base classes, etc. But, overall it enabled progress that would have otherwise been stalled.