Теория языков программирования (немного об интерпретаторах, триадах, оптимизации, парсерах и прочее)
Compilers construction some lectures of whole course, it covers some methods on interpreters, optimisations, antlr, dsl (introduction)
3. Компиляторы и интерпретаторы
- Компиляторы создают объектный код или внутреннее представление
- Интерпретаторы на лету выполняют исходный код или команды виртуальной
машины
-
Примеры компиляторов и компилируемых языков
- C (gcc)
- C++ (g++)
- C# (csc)
- javac (компилятор в class файлы)
- erlc (компилятор в beam платформы Erlang)
Примеры интерпретаторов и интерпретируемых языков
- Python
- PHP
- JS
- java (интерпретатор байт-кода.
Как правило, интерпретированные языки отличаются динамической типизацией
(тип вычисляется в процессе выполнения и заранее неизвестен)
В целях оптимизации часть кода может кешироваться
4. Как получить интерпретатор из
синтаксического и семантического анализатора
- Кроме типа для всех переменных нужно хранить значение в узле дерева
- Для грамматических правил с выражениями нужно также возвращать тип и
значение, которое будет считаться в правилах вида A+A | A*A и тд
- Там же нужно делать преобразование типов по заданным правилам, в
зависимости от типа операндов
- Циклы делаются в соответствии с блок-схемой работы цикла (while -
вычисление и переход, for (A;B;C) D - присваивание, проверка, выполнение
тела, выполнение тела с инкрементом и тд
- Функции возвращают значения в своем узле. Для рекурсии необходимо
копирование поддеревье с подчисткой и заменой на значение, иначе
значение затрётся
- См пособие для примеров!
Пример репозитория с исходным кодом интерпретатора Паскаля
(присваивания, функции, while)
https://github.com/SergeyStaroletov/PascalInterpreterMini
6. Кодогенерация
● Напрямую в ассемблерный или объектный код
● В промежуточный код (или последовательность
промежуточных)
Виды промежуточного кода
● Деревья
● ПОЛИЗ
● Триады/тетрады
7. Кодогенерация (далее наша цель)
Нужно всегда думать о том, что мы будем генерировать на
выходе
gcc -S 1.c
13. Тетрады и Триады
Тетрада – это конструкция, содержащая:
- Операция
- Операнд 1
- Операнд 2
- Результат
Триада – это пронумерованная конструкция, содержащая
- Операция
- Операнд 1 (или ссылка)
- Операнд 2 (или ссылка)
15. Пара примеров
Выражения
Добавляем в множество триад
триаду с присваиванием
на адрес триады с
результатом
выражения
Добавляем
в триады
+ <триаду выр1> <триаду выр 2>
21. Виды оптимизаций
Оптимизация вместе с профилированием кода (опции gcc -pg, clang -fprofile-instr-generate)
позволяет улучшать и контролировать скорость критических строк кода, предоставляя
компилятору возможности делать предопределенные классы оптимизаций
22. Граф управления
Если мы имеем последовательность триад, то легко
построить граф управления по ней
23. Визуализация графов управления
Graphviz (dot). Описание графа на простом декларативном
языке приводит к хорошим, выровненным графам
https://dreampuf.github.io/GraphvizOnline
25. Простейшие алгоритмы оптимизации
Удаление повторяющихся триад – 1.
Попарно сравниваем триады, при условии что между
Сравниваемыми триадами не изменяются их операнды
26. Простейшие алгоритмы оптимизации
Удаление повторяющихся триад – 2.
- Поиск поддеревьев в графе
- Передача операций вверх, формирование “хэша” -
агрегата сложных операций (с учетом коммуникативности)
27. Оптимизация ветвлений
- Поиск одинакового начала и конца веток if then и else
- Одинаковые начала надо вставить до цикла
- Одинаковые концы – после цикла
28. Оптимизация циклов
- Главное сначала определить, где цикл
- Поиск инвариантных триад (значений, которые не
меняются внутри цикла) – их нужно вычислить до цикла и
потом просто использовать
- Предсказание изменения счетчика цикла и более
эффективный пересчет выражений с ним
29. Оптимизация используемых регистров
- Оптимально вычислять выражения, когда операнды
загружены в регистры процессора
- Регистров мало
- Приходится подгружать из памяти
- Поэтому для последующей
генерации кода нужно
использовать минимум регистров
Вводим функцию reg(a) для каждой вершины, которая
будет хранить число регистров
31. Другие алгоритмы
- Превращение малых функций в inline
- Поиск неиспользуемых переменных путем формирования
графа использования каждой переменной
- Векторизация, автоматическая параллелизация кода
Меня интересуют например
- Устранение рекурсии типа Факториал
- Устранение рекурсии типа Фибоначчи*
36. Процесс кодогенерации
Будем проводить кодогенерацию asm кода по триадам
Принцип “обезьяна видит – обезьяна делает”
Monkey-driven developing :)
Нужно изучать, как ведет себя компилятор gcc (или другой) путем тестов
А именно:
- какие команды генерируются для арифметики
- команды регистр-регистр, регистр-память
- какие команды для переходов
- методы адресации (прямая, косвенная, относительно стека)
- вызов функций, передача параметров
- вызов методов стандартной библиотеки, если надо вывод
38. Отсюда делаем вывод
Для печати значения можно применять
leaq L_.str(%rip), %rdi
movl <что>, %esi
movb $0, %al
callq _printf
xorl %esi, %esi
##
# в самом конце
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "%dn"
Данный код зависит от разрядности и платформы!
39. Особенности ассемблера
- Загрузка значений в другом порядке
- Константы с $
- Знаковые/беззнаковые операции
- Есть команды только регистр-регистр, есть регистр-
память. Нужно использовать movl предварительно в
некоторых случаях
- Зависит от платформы, для которой генерируем!
Для другой ОС генерация отличается в некоторых моментах
(пролог/эпилог/описания)
40. LLVM: решаем проблему зависимости от
платформы
Сегодня все чаще генерация идет через специальный
бэкэнд в виде промежуточного кода с возможностью
генерации под разные ОС/платформы/языки
gcc -S -emit-llvm 1.c
41. Приемы генерации
- Генерация описания (глобальные, не глобальные)
- Локальные переменные – генерируем по триаде начала
функции резервирование стека, далее адресуем переменные
относительно стека+смещение
- Для генерации арифметики храним, где значение
переменной/триады. Оптимально использовать регистры. Для
этого есть пул регистров, которые по очереди “занимаются”.
Для каждой триады известно, где сейчас ее значение. При
ссылке на нее оттуда его берем. Если нужен регистр, а все
заняты, нужно слить значение из него - генерируем выгрузку по
необходимости и загрузку в регистр нового перед операцией с
значением соответствующей триады
44. Проблемы
- Написание анализаторов, интерпретаторов,
компиляторов – достаточно стандартная задача
- Все зависит от входной грамматики; алгоритмы анализа,
интерпретации, компиляции стандартны и их можно
комбинировать
- Grammar-Driven Development
45. BNF и EBNF формы грамматик
Форма Бэкуса — Наура:
В РБНФ введены два новых синтаксических элемента:
условное вхождение (выражение в квадратных скобках) и
повторение (выражение в фигурных скобках)
46. Процесс разработки
Грамматика в виде БНФ/ЕБНФ
(свой язык описания)
Генератор
Внедренные конструкции
семантики на целевом языке
программирования
Код парсера,
интерпретатора,
компилятора
47. Генераторы, принятые в Community
- Java Compiler Compiler
- ANTLR (Java world)
- Yacc/Flex/Bison (GNU world)
48. Преимущества использования генераторов
- Нет изобретению велосипеда
- MDD-процесс (модель это грамматика), поддерживаемость
- Интегрируемость в IDE
- Визуализация синтаксических диаграмм
- Открытые грамматики для реальных языков
программирования
- Отладка по грамматике (дебаг не по коду, а по исходнику
грамматики)
49. Генераторы анализаторов и стратегии разбора
- ANTLR работает с LL(*) грамматиками
- YACC работает с LALR (LR) грамматиками (более
эффективно работает с обработкой ошибок в синтаксисе)
51. Примеры: ANTLR плагин для IDEA
!! Имеется большое число доступных грамматик реальных языков (с расширением .g4)
Правила
грамматиики
Нетерминалы
Код для теста
Дерево
разбора
Сгенерированный код
парсера в проекте
52. Пример генератора с ANTLR
1. Добавляем в проект antlr-4.7.2-complete.jar
2. Ищем и качаем грамматику Java (в примере - java9.g4)
3. Правой кнопкой в грамматике “Generate ANTLR recognizer”
4. В проект добавлены Java9Parser, Java9Lexer, Java9Visitor
5. Пишем main(), нужно начать с корневого правила грамматики
6. И добавить свой Listener для обработки захода или выхода из узлов
синтаксического дерева
53. Пример генератора с ANTL
7. Теперь нужно написать актуальный код по обработке заходов в узлы - мы
будем считать размер методов
8. По грамматике это MethodDeclaration
9. Пишем код при выходе из описания метода
54. DSL языки
DSL (Domain-Specific Language) – миниязык, который
описывает понятия и взаимосвязи из некоторых предметных областей
Примеры: управление роботами*, персонажами,
умными домами переводом денег, …
Терминалы языка – понятия этой области + операции над ними
Современные языки позволяют доопределять синтаксис для создания подобных DSL языков
А также существуют готовые решения на основе текстового, графического языка и
генератора/интерпретатора по ним. XText+Xtend на основе Eclipse - готовое средство создание
своих DSL с метамоделью языка
* Никитин, Старолетов. ИСПОЛЬЗОВАНИЕ KOTLIN ДЛЯ ОПИСАНИЯ ДЕРЕВЬЕВ ПОВЕДЕНИЯ В ЦЕЛЯХ
МОДЕЛИРОВАНИЯ ИИ В ОБЛАСТИ ОБУЧАЮЩЕГО ПО
55. Задание (расчетка)
- Используя один из генераторов парсеров, использовать
его для создания парсера реальной грамматики языка
программирования.
- Создать свою мини-грамматику в нем
- Реализовать семантику - интерпретатор выражений + if/
while/case
- Вставить в отчет диаграммы разбора, генерируемые
средством