Пишем свой собственный компилятор или заново изобретаем SASS
Профессия разработчика прекрасна не только тем что ей можно обучиться самому проходя онлайн-курсы или читая книги. Но и получить небольшой опыт самостоятельно: написать свой аналог (или доработку) существующей библиотеки или продукта “just for fun” и залит его на github.
Лично я убежден, что каждый разработчик должен потратить личное время чтобы написать свою CMS, интерпретатор/компилятор или свою js-библиотеку для создания интерактивных интерфейсов 😎. Это очень хорошая самостоятельная практика языка и доведения проекта до конца.
Свою CMS я писал дважды, так что пришло время написать компилятор.
Зачем нужно писать компилятор/интерпретатор?
Каждый из этих инструментов решает свою специфичную задачу.
Компилятор выполняет задачу преобразования одного языка в другой без потери смысла. Это необходимо когда среда, в которой пишется код и то где он будет выполняться — разная.
Более конкретный пример: Современные браузеры понимают только язык javascript, но это не повод писать на нем клиентскую часть. Есть много языков которые компилируются в него.
Интерпретатор выполняет задачу обработки и мгновенного выполнения исходной программы или запроса, без компиляции его в другой язык. Есть много интерпретируемых языков программирования, таких как Ruby, Python, Javascript и др.
Более конкретный, рабочий пример: Отдел маркетинга каждый день посылает запрос IT-отделу на выгрузку данных из БД в csv-файлы. При этом форматы метрик, по которым надо сделать выгрузку, могут часто меняться. Чтобы не продолжать нагружать программистов каждодневным переписыванием кода, можно реализовать интерпретатор, который будет представлять собой ограниченную версию SQL и будет более понятен типичному сотруднику. Благодаря этому отдел маркетинга получает возможность делать выгрузки самостоятельно и быстро.
Пишем свой компилятор
Перед тем как начать писать, нужно было определиться с функционалом.
При выборе функционала компилятора стояли следующие требования:
- Он не должен быть слишком простой, чтобы его было скучно делать
- Он не должен быть сложным, чтобы тратить на него все свободное время
Идеи о создании компилятора языка программирования отошли на второй план. Был выбор между написанием аналога препроцессинга SASS или шаблонизатора HAML/SLIM. В итоге был выбран SASS.
Осталось определиться с функционалом. Я поставил цель реализовать следующие фичи:
- Использование многострочных комментариев
- Использование переменных
- Использование миксинов
- Вложенность селекторов
Но и появился ряд ограничений:
- Переменные и миксины не могут быть вложенные
- Миксины не имеют входящих аргументов
Структура приложения
Само приложение представляет собой механизм компиляции (machine) и CLI (консольный интерфейс).
Консольный интерфейс — это стандартный bin-файл. А механизм компиляции состоит из трех частей:
- Парсер — здесь логика преобразования текста в абстрактное синтаксическое дерево (AST).
- Трансформатор — здесь логика преобразования вложенного синтаксического дерева в не вложенную структуру.
- Компилятор — здесь логика компиляции (отрисовки) синтаксического дерева в файл.
Парсер
Это часть системы, которая преобразует исходный текст в абстрактное синтаксическое дерево. Это дерево отражает смысловую и синтаксическую структуру исходного текста для дальнейшей обработки.
Иными словами: нужен инструмент который способен преобразовать сплошной текст в набор коротких, последовательный и вложенных процедур. Относительно которых можно легко описать правила взаимодействия машины с ними. Парсер выполняет задачу: “преобразуй этот текст в набор инструкций которыми будет удобно манипулировать машине”.
Более подробно про его работу:
И так, у нас есть файлик с css, который надо преобразовать.
Парсер проходит по тексту, удаляет комментарии, разбивает текст на минимальные логические части. В данном случае, для разделения текста на минимальные логические части, достаточно разбить текст относительно символов ‘;’, ‘{‘, ‘}’. В итоге, текст преобразовывается в массив строк.
После этого парсер проходит по каждой логической части и оборачивает его в абстрактный узел. Это дает нам набор последовательных процедур. Массив строк преобразовался в массив объектов, которыми можно манипулировать, но объекты пока не являются вложенными.
После этого парсер преобразует предыдущий массив в абстрактное синтаксическое дерево (AST). У нас появилась вложенная структура, которой легко манипулировать для дальнейшей обработки.
Вот пример работы для более сложной структуры.
До (scss файл):
После (AST):
Трансформатор
Это часть системы которая преобразует синтаксическое дерево абстрактных узлов, в набор процедур. Именно они будут понятны компилятору для запуска отрисовки. Плюс, в нем находится логика преобразования вложенных селекторов в не вложенную структуру, так как CSS не поддерживает вложенные селекторы.
Иными словами: Трансформатор выполняет задачи:
- Пройдись по дереву и если увидишь где-то вложенность, то преобразуй ее в не вложенную структуру.
- Пройдись по дереву и преобразуй каждый абстрактный узел в конкретную процедуру которую сможет отрисовать компилятор.
Файл с таким содержимым:
После прохождения парсера и трансформатора предоставит следующую структуру:
Более сложный пример.
До:
После:
Компилятор
Это часть системы, которая проходит по абстрактному синтаксическому дереву и на основе его структуры отрисовывает css-файл.
Обход дерева проходит в два этапа:
Первый: обойди все дерево и преобразуй узлы которые подвержены преобразованиям. Преобразования могут быть следующие:
- Значения переменных сохраняются во временное хранилище.
- В значениях свойств в которых используются переменные, подставляются значения из хранилища.
- Свойства миксинов тоже сохраняются во временное хранилище.
- В селекторы, внутри которых используются миксины, подставляются значения нужных свойств из хранилища.
Второй: запускается обход преобразованного дерева и происходит отрисовка тех узлов которые нужно отрисовать (только селекторы и их свойства).
После всех этих нехитрых манипуляций, мы получаем валидный css-файл.
Заключение
Конечно, данный функционал недотягивает до настоящего SASS и данная реализации менее стабильная в работе. Но данный опыт был интересным, небольшой персональный челлендж на проверку собственных сил. Особенно забавно не просто реализовывать сложный функционал, а делать это в соответствии со стайл-гайдами, благо есть такие инструменты как rubocop и rubycritic.
Весь исходный код выложен на github. Пример использования можно прочесть в README в разделе “Example of usage”. Исходные файлы хранятся в папке /source, скомпилированные файлы сохраняются в папку /css. https://github.com/kopylovvlad/lite_scss
Спасибо за время потраченное на прочтение данной статьи. “Knowledge must be shared”