ОБНОВЛЕНИЕ: Этот вопрос был темой моего блога за 4 февраля 2010 года . Спасибо за отличный вопрос!
Позвольте мне выложить это для вас. В самом простом смысле компилятор является «двухпроходным компилятором», потому что фазы, которые проходит компилятор:
1) Генерация метаданных .
2) Генерация IL .
Метаданные - это все материалы «верхнего уровня», которые описывают структуру кода. Пространства имен, классы, структуры, перечисления, интерфейсы, делегаты, методы, параметры типа, формальные параметры, конструкторы, события, атрибуты и так далее. По сути, все , кроме тел методов.
IL - это все, что входит в тело метода - фактический императивный код, а не метаданные о том, как код структурирован.
Первый этап фактически осуществляется через множество проходов через источники. Это больше, чем два.
Первое, что мы делаем, это берем текст источников и разбиваем его на поток токенов. То есть мы делаем лексический анализ, чтобы определить, что
class c : b { }
- это класс, идентификатор, двоеточие, идентификатор, левый вьющийся, правый вьющийся.
Затем мы выполняем «синтаксический анализ верхнего уровня», где мы проверяем, что потоки токенов определяют грамматически правильную программу на C #. Однако мы пропускаем тела метода разбора. Когда мы попадаем в тело метода, мы просто пролистываем токены, пока не доберемся до кудрявого близкого совпадения. Мы вернемся к этому позже; на данный момент мы заботимся только о том, чтобы получить достаточно информации для создания метаданных.
Затем мы делаем проход «объявление», где делаем пометки о расположении каждого пространства имен и объявления типа в программе.
Затем мы делаем проход, где проверяем, что все объявленные типы не имеют циклов в своих базовых типах. Сначала нам нужно сделать это, потому что при каждом последующем проходе мы должны иметь возможность обходить иерархии типов без необходимости иметь дело с циклами.
Затем мы делаем проход, где проверяем, что все ограничения универсальных параметров для универсальных типов также являются ациклическими.
Затем мы выполняем этап, где проверяем, является ли каждый член каждого типа - методы классов, поля структур, перечислимые значения и т. Д. - согласованными. Нет циклов в перечислениях, каждый переопределяющий метод переопределяет то, что на самом деле является виртуальным, и так далее. На этом этапе мы можем вычислить «vtable» макеты всех интерфейсов, классов с виртуальными методами и т. Д.
Затем мы делаем проход, где мы прорабатываем значения всех полей "const".
На данный момент у нас достаточно информации, чтобы выдать почти все метаданные для этой сборки. У нас до сих пор нет информации о метаданных для замыканий итераторов / анонимных функций или анонимных типов; мы делаем это поздно.
Теперь мы можем начать генерировать IL. Для каждого тела метода (и свойств, индексаторов, конструкторов и т. Д.) Мы перематываем лексер до точки начала тела метода и анализируем тело метода.
Как только тело метода анализируется, мы делаем начальный проход «связывания», где мы пытаемся определить типы каждого выражения в каждом операторе. Затем мы делаем целую кучу проходов по каждому телу метода.
Сначала мы запускаем проход, чтобы преобразовать циклы в goto и метки.
(Следующие несколько проходов ищут плохие вещи.)
Затем мы запускаем проход для поиска использования устаревших типов, для предупреждений.
Затем мы запускаем проход, который ищет использование анонимных типов, для которых мы еще не выдавали метаданные, и выдает их.
Затем мы запускаем проход, который ищет неправильное использование деревьев выражений. Например, используя оператор ++ в дереве выражений.
Затем мы запускаем проход, который ищет все локальные переменные в теле, которые определены, но не используются, для сообщения о предупреждениях.
Затем мы запускаем проход, который ищет недопустимые шаблоны внутри блоков итераторов.
Затем мы запускаем проверку достижимости, чтобы выдавать предупреждения о недоступном коде, и сообщаем вам, когда вы сделали что-то вроде забытого возврата в конце не пустого метода.
Затем мы запускаем проход, который проверяет, что каждое goto нацелено на разумную метку, и что каждая метка нацелена на достижимое goto.
Затем мы запускаем проход, который проверяет, что все локальные объекты определенно назначены перед использованием, отмечает, какие локальные переменные являются закрытыми над внешними переменными анонимной функции или итератора и какие анонимные функции находятся в достижимом коде. (Этот проход делает слишком много. Я уже давно собираюсь провести его рефакторинг.)
На данный момент мы закончили искать плохие вещи, но у нас еще есть много проходов, прежде чем мы заснем.
Далее мы запускаем проход, который обнаруживает отсутствующие аргументы ref для вызовов COM-объектов и исправляет их. (Это новая функция в C # 4.)
Затем мы запускаем проход, который ищет материал в форме «new MyDelegate (Foo)» и переписывает его в вызов CreateDelegate.
Затем мы запускаем проход, который преобразует деревья выражений в последовательность вызовов фабричных методов, необходимых для создания деревьев выражений во время выполнения.
Затем мы запускаем проход, который переписывает всю обнуляемую арифметику в код, который проверяет HasValue и т. Д.
Затем мы запускаем проход, который находит все ссылки формы base.Blah () и переписывает их в код, который выполняет не виртуальный вызов метода базового класса.
Затем мы запускаем проход, который ищет инициализаторы объектов и коллекций и превращает их в соответствующие наборы свойств и т. Д.
Затем мы запускаем проход, который ищет динамические вызовы (в C # 4) и переписывает их в сайты динамических вызовов, которые используют DLR.
Затем мы запускаем проход, который ищет вызовы удаленных методов. (То есть частичные методы без фактической реализации или условные методы, для которых не определен символ условной компиляции.) Они превращаются в no-ops.
Затем мы ищем недоступный код и удаляем его из дерева. Нет смысла в кодировании IL для этого.
Затем мы запускаем этап оптимизации, который переписывает тривиальные операторы "is" и "as".
Затем мы запускаем этап оптимизации, который ищет переключатель (константу) и переписывает его как ответвление непосредственно в правильный регистр.
Затем мы запускаем проход, который превращает конкатенацию строк в вызовы правильной перегрузки String.Concat.
(Ах, воспоминания. Эти два последних прохода были первыми вещами, над которыми я работал, когда присоединился к команде компиляторов.)
Затем мы запускаем проход, который переписывает использование именованных и необязательных параметров в вызовы, где все побочные эффекты происходят в правильном порядке.
Затем мы запускаем проход, который оптимизирует арифметику; например, если мы знаем, что M () возвращает int, и у нас есть 1 * M (), то мы просто превращаем его в M ().
Затем мы генерируем код для анонимных типов, впервые использованных этим методом.
Затем мы преобразуем анонимные функции в этом теле в методы классов замыкания.
Наконец, мы преобразуем блоки итераторов в конечные автоматы на основе коммутаторов.
Затем мы испускаем IL для преобразованного дерева, которое мы только что вычислили.
Легко, как пирог!