Транслятор построен на классической четырёхэтапной схеме: лексический анализ, синтаксический анализ, семантический анализ и генерация кода. Лексический анализатор порождается программой flex, синтаксический — программой bison; остальные этапы написаны на языке C.
Ключевое архитектурное решение: анализатор не печатает результат по ходу разбора, а строит абстрактное синтаксическое дерево. Между разбором и генерацией появляется место для семантического анализа, а генератор видит каждую конструкцию целиком и знает типы всех выражений. Именно это позволяет принимать решения, невозможные при построчном выводе.
| Исходный текст на C | Результат трансляции |
|---|---|
#include <stdio.h>
int main(void) {
int total = 0;
for (int i = 1; i <= 5; i++) {
total += i;
}
printf("total = %d\n", total);
printf("half = %d\n", total / 2);
printf("mean = %.1f\n", total / 5.0);
return 0;
} |
import sys
def main():
total = 0
for i in range(1, 5 + 1):
total += i
print("total = %d" % (total,))
print("half = %d" % (_c_div(total, 2),))
print("mean = %.1f" % (total / 5.0,))
return 0
if __name__ == "__main__":
sys.exit(main()) |
Обе программы печатают одно и то же:
total = 15
half = 7
mean = 3.0
Пример короткий, но три решения в нём уже нельзя принять без анализа.
Цикл сведён к range() — это безопасно, потому что счётчик объявлен
в заголовке и не используется после цикла. Деление total / 2 целое
и потому вынесено в _c_div, а total / 5.0 вещественное и осталось
обычным /: различить их можно только по типам операндов. Определение
_c_div транслятор добавляет в начало файла; оно разобрано ниже,
в разделе о целочисленном делении.
Требуются flex, bison, gcc и python (версии 3.6 и выше).
python tools/build.py собрать транслятор
python tools/build.py clean очистить каталог build
В Windows то же самое делает build.bat с теми же аргументами.
Транслятор собирается в build/c2py.exe:
c2py input.c -> output.py
c2py program.c program.c -> program.py
c2py program.c -o out.py явное имя результата
c2py program.c --stdout вывод на экран
c2py - < program.c чтение со стандартного ввода
Есть и Makefile, повторяющий ту же последовательность. Обратите
внимание: если в системе установлено несколько сборников gcc, make
может выбрать не тот. Тогда компилятор указывается явно:
make CC=/путь/к/gcc
Основным и проверенным способом сборки считается tools/build.py.
| Файл | Назначение |
|---|---|
src/lexer.l |
лексический анализатор (flex) |
src/parser.y |
грамматика и построение дерева (bison) |
src/ast.c |
узлы абстрактного синтаксического дерева |
src/types.c |
система типов и преобразования |
src/symtab.c |
таблица символов, области видимости, имена Python |
src/sema.c |
семантический анализ: типы, проверки, разметка дерева |
src/codegen.c |
генерация кода Python |
src/format.c |
разбор строки формата printf и scanf |
src/escape.c |
перекодирование escape-последовательностей |
src/diag.c |
диагностические сообщения с указанием места |
src/comments.c |
таблица комментариев исходного файла |
src/strbuf.c |
динамический строковый буфер |
Типы. int, char, short, long, float, double, void,
модификаторы signed, unsigned, const, одномерные массивы.
Объявления. Глобальные и локальные переменные, несколько описателей
в одном объявлении, инициализаторы, списки инициализаторов для массивов
(int a[] = {1, 2, 3}), объявления вперемешку с инструкциями (C99).
Функции. Определения с параметрами и возвращаемым значением, прототипы, рекурсия, вызов до определения.
Инструкции. if / else (включая цепочки), while, do-while,
for (все три части необязательны, допустимо объявление в заголовке),
switch / case / default, break, continue, return, блоки,
пустая инструкция.
Выражения. Полная лестница приоритетов языка C: присваивание и его
составные формы, тернарный оператор, ||, &&, |, ^, &,
сравнения, сдвиги, аддитивные и мультипликативные операции, унарные
+ - ! ~, префиксные и постфиксные ++ и --, индексация, вызов,
приведение типа, sizeof, оператор «запятая».
Литералы. Целые в десятичной, восьмеричной и шестнадцатеричной
записи с суффиксами; вещественные, включая экспоненциальную форму;
символьные и строковые со всеми escape-последовательностями, включая
\xHH и восьмеричные; склейка соседних строковых литералов.
Библиотека. printf, scanf, puts, putchar, abs, fabs,
sqrt, pow, floor, ceil, exit, strlen, rand, srand.
Ниже перечислены места, где буквальный перевод дал бы неверный результат.
В C деление и остаток усекают частное к нулю, а операции // и %
в Python округляют вниз. На отрицательных значениях результаты
расходятся: -7 / 2 равно -3 в C и -4 в Python.
Поэтому целочисленное деление переводится в вызов служебной функции
_c_div (и _c_mod для остатка). Если оба операнда — заведомо
неотрицательные константы, разницы нет, и выводится обычное //.
Служебные функции добавляются в результат только тогда, когда
действительно нужны.
Какая операция нужна, определяется типом операндов, а не их написанием.
Для этого и требуется семантический анализ: a / b при целых a и b
и при вещественных — это разные операции.
Приоритеты в Python отличаются от C. Сравнение там связывает слабее
побитовых операций, а not — слабее сравнения. Поэтому a & b == c
из C нельзя переписать теми же символами: получится другое дерево.
Генератор расставляет скобки по приоритетам Python и ставит их только
там, где они действительно нужны.
В C операции && и || дают 0 или 1, а and и or в Python
возвращают сам операнд: 7 and 2 равно 2. Пока значение используется
как условие, разница незаметна, поэтому в условиях выводятся обычные
and и or. В остальных случаях результат приводится к числу.
Цикл сводится к range() только тогда, когда это безопасно:
переменная объявлена в самом заголовке, шаг постоянен, а тело не меняет
ни счётчик, ни границу. Последнее существенно: в C граница вычисляется
заново на каждой итерации, а range() вычисляет её однажды.
Если переменная объявлена снаружи, используется цикл while. Причина
в том, что после цикла значения различались бы: C оставляет значение
после последнего приращения, а Python — последнее значение диапазона.
В форме while шаг цикла повторяется перед каждым continue — иначе
цикл стал бы бесконечным.
В C областью видимости является блок, а в Python — функция целиком.
Две разные переменные с одинаковым именем в соседних или вложенных
блоках получают разные имена в результате. Отдельно отслеживаются имена,
совпадающие с ключевыми словами Python (lambda, class, None и
другие) и с именами служебных функций.
Функция, изменяющая глобальную переменную, получает объявление global;
при чтении такое объявление не требуется и не добавляется.
Форматирование через оператор % в Python унаследовано от C, поэтому
%d, %5.2f, %-10s, %x, %o, %e, %g работают одинаково.
Строка формата не переписывается заново, а нормализуется: убираются
модификаторы длины (l, ll, h, z), а %u и %i заменяются
на %d. Так сохраняются ширина поля, точность и флаги.
Число аргументов сверяется со строкой формата на этапе семантического
анализа. Если строка заканчивается переводом строки, он не печатается
явно, а обеспечивается поведением print по умолчанию.
do-while разворачивается в while True с проверкой условия в конце.
switch превращается в цепочку if / elif; значение вычисляется один
раз во временную переменную; подряд идущие метки объединяются через
or; break, относящийся к switch, не переносится, а break цикла
сохраняется. Комментарии исходного текста переносятся в результат,
причём комментарий в конце строки остаётся в конце строки.
Перечисленные конструкции отвергаются с внятным сообщением, а не переводятся во что-то неверное:
- указатели и адресная арифметика; оператор
&допустим только в аргументахscanf; - структуры, объединения, перечисления, typedef, многомерные массивы;
++и--внутри выражения (j = i++): в Python нет такой операции, а её перенос потребовал бы временных переменных и менял бы порядок вычислений; допустимы отдельная инструкция и третья частьfor;- присваивание внутри другого выражения (
if (a = b)); цепочкаa = b = 0поддерживается, поскольку выражается и в Python; - провал между ветвями
switchбезbreak; breakв середине ветвиswitch;- директивы препроцессора, кроме
#include(он пропускается); gotoи метки.
Символьные массивы поддерживаются в двух видах: заданные строковым
литералом (char msg[] = "...") представляются строкой Python, заданные
списком — списком чисел.
Сообщения выводятся в формате, принятом в gcc, поэтому среды разработки разбирают их автоматически:
input.c:12:9: error: undeclared identifier 'total'
Транслятор сообщает о нескольких ошибках за один запуск: после ошибки разбор восстанавливается на ближайшей точке с запятой. При любой ошибке файл с результатом не создаётся.
MIT.