Компилятор javascript: Прошлое и будущее компиляции JavaScript / Habr – Компилятор на JavaScript с использованием ANTLR / Habr

Содержание

Прошлое и будущее компиляции JavaScript / Habr

Сейчас мы воспринимаем как должное быстрое выполнение js-кода в браузерах, и с каждым днем становится все больше вдохновляющих примеров того, что можно реализовать с помощью JS. Но так было далеко не всегда. В этой статье поговорим о JS-движках, отвечающих за компиляцию кода в браузерах, об их историческом пути ускорения и возможных будущих путях.

Первым движком, интерпретирующим js-код стал SpiderMonkey, который был представлен в браузере Netscape 2.0 в 1995 г. Миф о его быстром создании хорошо задокументирован. У Брендана Айка было всего 10 дней на дизайн языка и построение компилятора. Javascript был успешен с самого начала, и к августу того же кода Майкрософт уже встроила свою версию JScript в Internet Explorer 3.0. К концу 1996 язык был принят в комиссию для формальной стандартизации, и уже в июне следующего года обрел официальный стандарт ECMA-262. С тех пор поддержка JS стала обязательно для каждого браузера, и каждый крупный производитель начал строить свой движок для поддержки JS. В течение долгих лет эти движки развивались, заменяли друг друга, переименовывались, и становились основой для следующих движков. Отследить все созданные версии — задача не для слабых духом.


Например, мало кто сейчас помнит о браузере Konquerer от KDE, который использовал свой опенсорсный KJS движок. Впоследствии разработчики Apple “форкнули” этот проект и развили до будущего ядра WebKit, сменив в процессе эволюции несколько названий: Squirrelfish, Squirrelfish Extreme, Nitro.

Противоположные процессы также имели место быть. Есть движки, названия которых остались неизменными, в то время как все внутренности были изменены. Например, в SpiderMonkey от Mozilla нет никаких намеков на код, существовавший в 1995.

К середине 2000-х JavaScript был стандартизирован и очень распространен, но его исполнение было все еще медленным. Гонка за скоростью началась с 2008, когда появился ряд новых движков. В начале 2008 самым быстрым движком был Futhark от Opera. К лету Mozilla представила Tracemonkey, а Google запустил свой Chrome с новым JavaScript-джвижком V8. Несмотря на обилие названий, все они пытались делать одно и то же, и каждый проект хотел выгодно отличаться в скорости исполнения. Начиная с 2008 движки улучшались за счет оптимизаций своего дизайна, и между основными игроками происходила гонка за построение самого быстрого браузера.

Когда мы говорим о JavaScript-движке, мы обычно подразумеваем компилятор, и сделать генерируемый компилятором код более быстрым — вот что является настоящей задачей. Возможно, не все пишущие JS-программы задмываются о том, как работает компилятор.
Подразумевается, что JavaScript является языком высокого уровня. Это означает, что он читаем и имеет выскую степень гибкости. Работа компилятора — сформировать из этого человеко-читаемого кода нативный код.

Обычно компиляция проходит в 4 стадии:

1. На стадии лексического анализа (сканирования) компилятор сканирует исходный код и разбивает его на отдельные составляющие, называемые токенами. Обычно то достигается через регулярные выражения.

2. Парсер структурирует переработанный код в синтаксическое дерево.
3. Затем эта структура преобразуется транслятором в байткод. В простейшем случае трансляцию можно представить как маппинг токенов в их байткод представления.
4. В конце концов байткод проходит через интерпретатор байткода, чтобы получился нативный код.

Это классический дизайн компиляторов, и он применялся долгие годы. Но требования десктопных приложений сильно отличаются от требований браузеров, и эта архитектура стала испытывать трудности в ряде направлений. Устранение этих противоречий стало результатом гонки за скорость между браузерами.

Быстро, элегантно, правильно

JavaScript — очень гибкий язык и довольно толерантен к конструкциям “на грани фола”. Каким же образом писать компилятор для слабо типизированного, динамического языка с поздним связыванием? Перед тем как делать его быстрым, Вы должны сделать его аккуратным. Как выразился Брендан Айк,
“Быстро, элегантно, корректно. Выберите 2, учитывая, что ‘корректно’ уже выбрано”
“Fast, Slim, Correct. Pick any two, so long as one is ‘Correct’”

Jesse Ruderman из Mozilla создал очень полезный инструмент jsfunfuzz для тестирования корректности компилятора. Брендан назвал это пародией на JavaScript-компилятор, так как его цель создавать самые странные, но валидные конструкции, которые отправляются на проверку компилятору. Инструмент позволил выявить многочисленные крайние случаи и баги.

JIT компиляторы

Основная проблема с классической архитектурой в том, что интерпретация байткода во время выполнения довольно медленна. Производительность можно улучшить, выполнив шаг компиляции байткода в машинный код, но это потребует большого времени ожидания от пользователей при загрузке веб-страниц.

Решением является “ленивая компиляция”, или компиляция “на лету”. Как видно из названия, происходит компиляция кусков кода в машинный код имено к тому времени, как он понадобится. JIT-компиляторы появились в различных технологиях, с различными стратегиями оптимизации. Некоторые заточены под оптимизацию отдельных команд другие под оптимизацию повторяющихся операций, таких как циклы и функции. Современный JavaScript-движок применяет несколько таких компиляторов, работающих совместно для улучшения производительности вашего кода.

JavaScript JIT-компиляторы

Первым JavaScript JIT-компилятором стал TraceMonkey от Mozilla. Это был так называемый трассирующий JIT, так как он отслеживает наиболее повторяемые циклы. Эти “горячие циклы” компилируются в машинный код. Только благодаря одной этой оптимизации Mozilla получили улучшение производительности от 20% до 40% по сравнению с их предыдущим движком.

Вскоре после запуска TraceMonkey Google выпустил Chrome вместе с новым движком V8. V8 был разработан специально для оптимизации скорости. Основным архитектурным решением был отказ от генерации байткода, вместо чего транслятор генерирует напрямую нативный код. В течение года после запуска, команда также применила распределение регистров, улучшила инлайн кэширование, и полностью переписала движок регулярных выражений, сделав его в 10 раз быстрее. Это в совокупности увеличило скорость выполнения JavaScript на 150%. Гонка за скоростью продолжалась во всю!

Позднее производители браузеров представили компиляторы с дополнительным шагом. После того, как сгенерирован граф потока управления или синтаксическое дерево, компилятор может использовать эти данные для дальнейших оптимизаций перед генерацией машинного кода. Примерами таких компиляторов являются IonMonkey и Crankshaft.

Амбициозно целью всех этих преобразований является исполнение JavaScript кода на скорости нативного C. Еще несколько лет назад эта цель казалась невероятной, но разрыв в скорости исполнения все сокращается.

Теперь о некоторых частных особенностях компиляции JS.

Скрытые классы

Так как в JavaScript построение объектов и структур довольно просто для разработчика, навигация по этим нестрого детерминированным структурам может быть очень медленной для компилятора. Например, в C обычным способом хранения свойств и обращения к свойствам является хэш-таблица. Проблема с хэш-таблицей в том, что поиск по очень большой хэш-таблице может быть очень медленным.
Для ускорения этого процесса и V8, и SpiderMonkey применяют скрытые классы — внутреннее представление ваших JavaScript объектов. В Google их называют maps, а в Mozilla — shapes, но это по сути одно и то же. Эти структуры гораздо быстрее в поиске, чем стандартный поиск по словарю.
Вывод типов

Динамическая типизация Javascript — это то, что позволяет одному и тому же свойству быть числом в одном месте и строкой- в другом. К сожалению, такое разнообразие приводит к многочисленным дополнительным проверкам типов в компиляторе, а код с условными проверками намного длиннее и медленнее, чем код, определенный для типов переменных.

Решением является метод вывода типов, который есть во всех современных JS-компиляторах. Компилятор делает предположения о типах данных свойств. Если преположение верно, он передает выполнение типизированному JIT, который генерирует быстрый машинны код для этих участков. Если же тип не совпадает, то код передается нетипизированному JIT, для выполнения на более медленном коде с проверками условий.

Инлайн кэширование

Это самая распространенная оптимизация в современных JavaScript-компиляторах. Это не новая техника ( впервые была применена 30 лет назад для Smalltalk компилятора), но очень полезная.

Инлайн кэширование требует обе предыдущие техники для своей работы: вывод типов и скрытые классы. Когда компилятор обнаруживает новый класс, он кэширует его скрытый класс вместе со всеми определенными типами. Если эта структура встречается позже, ее можно быстро сравнить с сохраненным кэшем. Если структура или тип данных изменились, они передаются в более медленный обобщенный (generic) код или в некоторых компиляторах выполняется полиморфное инлайн кэшировние — генерация отдельного кэша одной структуры для каждого типа данных. Подробнее об этом можно прочитать в статье Вячеслава Егорова из команды V8.

Когда компилятор получает структуру кода и типы данных в ней, становятся возможными разнообразные дополнительные оптимизации. Вот лишь несколько примеров:

inline expansion, или “inlining”

Вызов функции является дорогой операцией, так как требует какого-то вида поиска, а поиск может быть медленным. Идея в том, чтобы поместить код тела функции в то место, где она вызывается. Это позволяет избежать лишнего ветвления, и ускоряет выполнение, но за счет увеличения размера выполняемого кода.
инвариантные изменения циклов, “подъем”

Циклы являются первым кандидатом на оптимизацию. Убрав ненужные вычисления из цикла, можно сильно улучшить производительность. Самый простой пример: цикл for по элементам массива. Вычислять длину массива на каждой итерации невыгодно, поэтому эта операция выносится, “поднимается” за цикл.
свертка констант

Вычисляются константные выражения, а также выражения, содержащие неизменяемые переменные.
удаление общих подвыражений

Аналогично сверстке констант, компилятор ищет выражения, содержащие одинаковые вычисления. Эти выражения могут заменяться на переменные с рассчитанными значениями.
устранение мертвого кода

Код, который не используется, или его невозможно достичь. Нет смысла оптимизировать тело функции, которая ни разу не используется, ее можно просто удалить.

Это лишь небольшой набор простых средств, демонстрирующий в каком направлении работают производители браузеров, чтобы достичь своих амбициозных целей. Многие из них сделали долгосрочные инвестиции в концепцию веба, как операционной системы. Чтобы достичь этого, они поставили задачу выполнения JavaScript кода со скоростью нативного C, и постепенного стирания разницы между нативными и веб-приложениями.

ES.next

Следующая версия спецификации EcmaScript ( EcmaScript 6) уже давно в работе, финальная версия ожидается в этом году. Одной из обозначенных целей проекта является быстрая компиляция. Обсуждается набор средств, которыми это можно достичь, включая типизацию, бинарные данные и типизированные массивы. Типизированный код может напрямую отправляться в JIT, ускоряя время компиляции и исполнения.

Поодержка ES.next браузерами еще довольно ограничена, но за этим можно следить хотя бы здесь, также можно начать тестирование с помощью Traceur – компилятора ES.next в JavaScript, написанный на JavaScript.

WebGL

JavaScript в браузере не ограничен манипуляциями с DOM. Большое число современных браузерных игр рендерятся напрямую на canvas элементе страницы, используя стандартный 2D-контекст. Самый быстрый способ рендеринга на канвасе — WebGL, API обеспечивающее оптимизацию за счет переноса дорогих операция на GPU, оставляя CPU для логики приложения.

WebGL в каком-то виде поддерживается в большинстве браузеров, в первую очередь в Chrome и Firefox. Пользователи Safari и Opera должны сначала включить соответствующую опцию. Microsoft также недавно объявили о поддержке WebGL в IE11.

К сожалению, даже с полноценно поддержкой браузеров, нельзя гарантировать, что WebGL будет работать одинаково хорошо для всех ваших пользователей, так как это зависит еще и от современных драйверов GPU. Google Chrome является единственным браузером, предлагающим альтернативное решение, если этих драйверов не установлено. WebGL — очень мощная технология, но ее звездный час еще не настал. Помимо вопросов безопасности, поддержка мобильных устройств очень неоднородна. И, конечно, в старых браузерах нет никакой поддержки.

Javascript как результат компиляции

Несмотря на то, что все современные веб-приложения используют JavaScript на клиенте, не все они были написаны на JavaScript. Многие написаны на абсолютно отличных языках (Java, C++, C#), и затем компилированы в JS. Некоторые же были созданы как языки, расширяющие JavaScript, для более удобной разработки, например TypeScript.

Кросс-компиляция, однако, имеет свои проблемы. Минифицированный код нечитаем, его сложно отлаживать, на практике это возможно лишь для браузеров с поддержкой сорс-маппинга — промежуточного файла, сохраняющего маппинг в код на исходном языке.
Пару лет назад Скотт Хансельман из Microsoft выдвинул постулат о том, что Javascript является языком компиляции для веба. Его замечание о том, что современнное минифицированное JavaScript приложение плохо читаемо, сложно оспаривать, но его пост тем не менее вызвал большую дискуссию. Многие веб-разработчики начинали с того, что просто изучали исходный код в браузере, а сейчас он практически всегда обфусцирован. Можем ли мы по этим причинам потерять часть будущих разработчиков?

Интересной демонстрацией идеи стал проект Emscripten, которы позоляет компилировать байткод LLVM в JavaScript. LLVM(Low Level Virtual Machine) является очень популярным форматом промежуточной компиляции, можно найти LLVM компилтор практически для любого языка. Такой подход позволит каждому писать исходный код на том языке, на котором ему удобно. Проект все еще в ранней стадии, но команда уже выпустила ряд впечатляющих демо. Например, разработчики Epic портировали Unreal Engine 3 в JavaScript и WebGL, используя LLVM компилятор C и Emscripten для компиляции в asm.js код.

asm.js

Проект, работающий в этом же направлении. Его создатели приняли призыв “javascript как машинный код” довольно буквально, взяв в качестве ассемблера JavaScript сильно ограниченное подмножество языка. таким образом теоретически можно писать asm.js код руками, но никто не захочет этого делать. Чтобы извлечь максимум пользы из этой возможности, вам потребуется 2 компилятора.
Компилятор Emscripten может производить код asm.js. результирующий JavaScript нечитаем, но он корректен и обратно совместим. Огромное ускорение прийдет тогда, когда движки браузеров будут распознавать формат asm.js и пропускать этот код через отдельный компилятор. Для этой цели в Mozilla работают над OdinMonkey, оптимизирующий asm.js компилятор, встраиваемый в IonMonkey. Google также заявил о поддержке asm.js в Chrome. Предварительные тесты показали производительность примерно в 50% от скомпилированного C++, это феноменальное достижение, сравнимое по скорости с Java и C#. Команда уверена, что результат будет улучшен.
Mozilla Research действительно находится на гребне волны в настоящее время. В дополнение к Emscripten и asm.js, также есть проект LLJS (JavaScript как C), а также совместно с Intel идет разработка River Trail – расширений ECMAScript для параллельных вычислений. Учитывая, как много усилий прикладывается в этом направлении, и какие уже результаты получены, можно предположить что исполнение JavaScript на нативной скорости не так недостижимо, как казалось раньше.
ORBX.js

Есть также те, кто предлагают решать проблему производительности JavaScript за счет полной виртуализации. Вместо того, чтобы запускать приложение на своей машине, оно запускается в облаке. Это, конечно, не решение самой проблемы компиляции JS, а альтернативное решение для пользователей. ORBX.js — реализация видеокодека, способного делаь стриминг видео с разрешением 1080 пикселей исключительно средствами JavaScript. Совместный проект Mozilla и Otoy.
Технология, конечно, впечатляет, но, возможно, создает больше проблем, чем решает.

А что Вы думаете о будущем компиляции Javascript?

компилятор из JavaScript в JavaScript / Habr

JavaScript — это ассемблер Веба. В мире существуют десятки проектов, которые компилируют код на C++, Java, C#, Python, Ruby или любом другом языке в JavaScript. Практически не осталось языка, который невозможно скомпилировать в JavaScript. Ну разве что кроме… самого JavaScript!

Мы решили исправить эту ситуацию. Встречайте: js2js — революционный компилятор, который компилирует JavaScript в JavaScript!
Ключевые особенности проекта

Контроль: вы получаете полный контроль над каждой строчкой результирующего кода. Вы даже можете скомпилировать неработающую программу на JavaScript в идентичную ей неработающую программу.
Нулевой оверхед: js2js не замедлит исполнение кода, скомпилированный код будет иметь точно такую же производительность как и исходный.
Интеграция: js2js работает с любой другой существующей технологией связанной с JavaScript: AngularJS, Emscripten, Vanilla JS, jQuery, CoffeeScript, TypeScript, и даже Dart.
Совместимость: код, сгенерированный js2js поддерживает все виды стационарных, мобильных устройств и браузеров. Даже если вы пишете под Netscape 2.0, вы можете скомпилировать свой код в js2js.

Использование

Код проекта доступен на github и работает на NodeJS. Для того чтобы скомпилировать ваш код просто введите в консоли команду:
node js2.js -i <input_file_or_directory> -o <output_file_or_directory>

Например, можно скомпилировать сам компилятор:
>node js2.js -i js2.js -o js2.compiled.js -v
Welcome to js2js compiler.
Compiling js2.js...
Output is written to: js2.compiled.js
Done!
Как я могу помочь проекту?

Код проекта доступен по лицензии MIT, а значит вы можете присылать свои pull-requests, развивать свою ветвь проекта, использовать его в своих коммерческих и не очень проектах или просто рассказать о нем своим друзьям.

Как быть* компилятором — создание компилятора на JavaScript / Habr

Привет, Хабр! Представляю вашему вниманию перевод статьи «How to be* a compiler — make a compiler with JavaScript» автора Mariko Kosaka.

*Все верно! Быть компилятором — это здорово!

Дело было одним замечательным воскресеным днем в Бушвике, Бруклин. В моем местном книжном магазине я наткнулась на книгу Джона Маэда “Design by Numbers”. Это была пошаговая инструкция по изучению DBN — языка программирования, созданного в конце 90-х в MIT Media Lab для визуального представления концепций компьютерного программирования.

Приведенные на первом рисунке три строки кода рисуют черную линию на белой бумаге (пример взят отсюда). Для отрисовки более сложных фигур, таких как, например, квадрат, требуется нарисовать просто больше линий (второй рисунок).

Я сразу подумала, что в 2016 году, это могло бы стать интересным проектом — создавать SVG из DBN без установки Java для выполнения исходного DBN кода.

Я решила, что для этого мне нужно написать компилятор из DBN в SVG, таким образом мой квест написания компилятора начался. Создание компилятора звучит как довольно сложный научный процесс, но я даже никогда не писала обход графа на интервью… Смогу ли я написать компилятор?

Попробуем сначала сами стать компилятором


Компилятор — это механизм, который берет кусок кода и преобразует его во что-то другое. Давайте скомпилируем простой DBN код в физический рисунок.

Возьмем три DBN команды: Paper задает цвет бумаги, Pen задает цвет кисти и Line рисует линию. Цвет 100 равнозначен 100%-ному черному цвету, что есть rgb(0%, 0%, 0%) в CSS. Изображения созданные в DBN всегда находятся в градации серого. В DBN бумага всегда 100×100, ширина линии всегда 1, а сама линия задается (x, y) координатами, отсчет ведется от нижнего левого угла изображения.

Давайте остановимся на этом и попробуем сами побыть компилятором. Возьмите бумагу, ручку и попробуйте скомпилировать следующий код в рисунок.

Paper 0
Pen 100
Line 0 50 100 50

Вы нарисовали горизонтальную линию от левого края листа до правого, и она находится в центре по вертикали? Поздравляю! Вы только что стали компилятором.

Как же работает компилятор?


Давайте разберемся, что происходило в нашей голове, пока мы были компилятором.

1. Лексический анализ (Tokenization)


Первое, что мы сделали, разбили исходный код на слова по пробелам. В процессе мы условно определили примитивные типы для каждого токена, такие как “слово” или “число”.

2. Синтаксический анализ (Parsing)


Как только мы разбили фрагмент текста на токены, мы прошли по каждому из них и попытались найти взаимосвязь между ними.

В данном случае мы сгруппировали вместе набор чисел и относящееся к ним слово. Сделав это, мы стали различать определенные структуры кода.

3. Трансформация (Transformation)


После синтаксического анализа, мы трансформировали полученные структуры во что-то более подходящее под конечный результат. В нашем случае мы собираемся нарисовать изображение, значит нам нужно преобразовать наши структуры в пошаговую инструкцию, понятную человеку.

4. Генерация кода (Code Generation)


На этом этапе мы просто следуем инструкциям, которые мы сделали на предыдущем шаге подготовки к рисованию.

Именно этим занимается компилятор!

Рисунок, который мы сделали, это и есть скомпилированный результат (аналогично .exe файлу, который создается в процессе компиляции программы на C). Мы можем отправить этот рисунок любому человеку или на любое устройство (сканер, камера и т.п.) и все распознают черную линию в центре листа.

Давайте напишем компилятор


Теперь, когда мы знаем, как работает компилятор, давайте напишем еще один, но уже с использованием JavaScript. Этот компилятор будет брать DBN код и преобразовывать его в SVG.

1. Lexer function


Точно так же, как мы можем разделить предложение “У меня есть ручка” на слова [У, меня, есть, ручка], лексический анализатор может разбить представленный в виде строки код на определенные осмысленные части (токены). В DBN все токены разделены пробелами и классифицируются как “слово” или “число”.
function lexer (code) {
  return code.split(/\s+/)
          .filter(function (t) { return t.length > 0 })
          .map(function (t) {
            return isNaN(t)
                    ? {type: 'word', value: t}
                    : {type: 'number', value: t}
          })
}
input: "Paper 100"
output:[
  { type: "word", value: "Paper" }, { type: "number", value: 100 }
]

2. Parser function


Парсер проходит по каждому токену, собирает синтаксическую информацию и строит так называемое абстрактное синтаксическое дерево (Abstract Syntax Tree). Вы можете рассматривать AST как карту нашего кода — способ увидеть как он структурирован.

В нашем коде два синтаксических типа “NumberLiteral” и “CallExpression”. NumberLiteral означает, что значение — это число. Это число используется в качестве аргумента для CallExpression.

function parser (tokens) {
  var AST = {
    type: 'Drawing',
    body: []
  }

  // Циклический перебор токенов
  while (tokens.length > 0){
    var current_token = tokens.shift()

    // Так как числовой токен сам по себе ничего не делает,
    // мы анализируем синтаксис только тогда, когда находим слово
    if (current_token.type === 'word') {
      switch (current_token.value) {
        case 'Paper' :
          var expression = {
            type: 'CallExpression',
            name: 'Paper',
            arguments: []
          }
          // Если текущим токеном является CallExpression типа Paper,
          // следующий токен должен быть аргументом цвета
          var argument = tokens.shift()
          if(argument.type === 'number') {
            expression.arguments.push({  // Добавить информацию об аргументе в объект выражения
              type: 'NumberLiteral',
              value: argument.value
            })
            AST.body.push(expression)    // Добавить наше выражение в АСТ
          } else {
            throw 'Paper command must be followed by a number.'
          }
          break
        case 'Pen' :
          ...
        case 'Line':
          ...
      }
    }
  }
  return AST
}
input: [
  { type: "word", value: "Paper" }, { type: "number", value: 100 }
]
output: {
  "type": "Drawing",
  "body": [{
    "type": "CallExpression",
    "name": "Paper",
    "arguments": [{ "type": "NumberLiteral", "value": "100" }]
  }]
}

3. Transformer function


Абстрактное синтаксическое дерево (AST), что мы создали на предыдущем шаге, хорошо описывает, что происходит в коде, но мы все еще не можем создать из этого SVG.

Например, команда “Paper” понятна только коду, написанному на DBN. В SVG для представления бумаги мы бы хотели использовать <rect> элемент, поэтому нам нужна функция, которая сконвертировала бы наше AST в другое AST, более подходящее для SVG.

function transformer (ast) {
  var svg_ast = {
    tag : 'svg',
    attr: {
      width: 100, height: 100, viewBox: '0 0 100 100',
      xmlns: 'http://www.w3.org/2000/svg', version: '1.1'
    },
    body:[]
  }
  
  var pen_color = 100 // Цвет по умолчанию - черный

  // Циклический перебор выражений
  while (ast.body.length > 0) {
    var node = ast.body.shift()
    switch (node.name) {
      case 'Paper' :
        var paper_color = 100 - node.arguments[0].value
        svg_ast.body.push({ // Добавить информацию о rect элементе в тело svg_ast
          tag : 'rect',
          attr : {
            x: 0, y: 0,
            width: 100, height:100,
            fill: 'rgb(' + paper_color + '%,' + paper_color + '%,' + paper_color + '%)'
          }
        })
        break
      case 'Pen':
        pen_color = 100 - node.arguments[0].value // Сохранить текущий цвет кисти в переменную `pen_color`
        break
      case 'Line':
        ...
    }
  }
  return svg_ast
 }
input: {
  "type": "Drawing",
  "body": [{
    "type": "CallExpression",
    "name": "Paper",
    "arguments": [{ "type": "NumberLiteral", "value": "100" }]
  }]
}
output: {
  "tag": "svg",
  "attr": {
    "width": 100,
    "height": 100,
    "viewBox": "0 0 100 100",
    "xmlns": "http://www.w3.org/2000/svg",
    "version": "1.1"
  },
  "body": [{
    "tag": "rect",
    "attr": {
      "x": 0,
      "y": 0,
      "width": 100,
      "height": 100,
      "fill": "rgb(0%, 0%, 0%)"
    }
  }]
}

4. Generator function


На последнем шаге компилятора вызывается функция, которая строит SVG код на основе нового AST, которое мы создали на предыдущем шаге.
function generator (svg_ast) {

  // Преобразовать объкет атрибутов в строку
  // { "width": 100, "height": 100 } преобразуется в 'width="100"'
  function createAttrString (attr) {
    return Object.keys(attr).map(function (key){
      return key + '="' + attr[key] + '"'
    }).join(' ')
  }

  // Верхним узлом всегда является <svg>. Создать строку с атрибутами для svg тэга
  var svg_attr = createAttrString(svg_ast.attr)

  // Для каждого элемента из тела svg_ast сгенерировать svg тэг
  var elements = svg_ast.body.map(function (node) {
    return '<' + node.tag + ' ' + createAttrString(node.attr) + '></' + node.tag + '>'
  }).join('\n\t')

  // Обернуть в открывающийся и закрывающийся svg тэг для завершения SVG кода
  return '<svg '+ svg_attr +'>\n' + elements + '\n</svg>'
}
input: {
  "tag": "svg",
  "attr": {
    "width": 100,
    "height": 100,
    "viewBox": "0 0 100 100",
    "xmlns": "http://www.w3.org/2000/svg",
    "version": "1.1"
  },
  "body": [{
    "tag": "rect",
    "attr": {
      "x": 0,
      "y": 0,
      "width": 100,
      "height": 100,
      "fill": "rgb(0%, 0%, 0%)"
    }
  }]
}
output:
<svg viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <rect x="0" y="0" fill="rgb(0%, 0%, 0%)">
  </rect>
</svg>

5. Соберем все вышесказанное вместе


Давайте назовем наш компилятор “sbn compiler”. Создадим sbn объект с нашими лексером, парсером, трансформатором и генератором. Добавим “compile” метод, который будет вызывать цепочку из 4 этих методов.

Теперь мы можем передать строку кода методу компиляции и получить SVG.

var sbn = {}
sbn.VERSION = '0.0.1'
sbn.lexer = lexer
sbn.parser = parser
sbn.transformer = transformer
sbn.generator = generator

sbn.compile = function (code) {
  return this.generator(this.transformer(this.parser(this.lexer(code))))
}

// Вызвать sbn компилятор
var code = 'Paper 0 Pen 100 Line 0 50 100 50'
var svg = sbn.compile(code)
document.body.innerHTML = svg

Я сделала интерактивное демо, в котором можно посмотреть результат работы компилятора на каждом их шагов. Код для sbn компилятора можно скачать на github. На данный момент я работаю над расширением функционала компилятора. Если вы хотите увидеть только базовый компилятор, собственно тот, что создавался в этой статье, вы можете найти его здесь.

Должен ли компилятор использовать рекурсию, обход и т.д.?


Да, все эти техники безусловно прекрасно подходят для создания компилятора, но это совершенно не значит, что вы сразу должны применить их в своем компиляторе.

Я начала писать свой компилятор, используя лишь небольшой набор команд DBN. Постепенно я стала усложнять функциональность, и сейчас собираюсь добавить в компилятор использование переменных, блоков и циклов. Это конечно хорошо иметь все эти конструкции, но не обязательно применять их с самого начала.

Писать компиляторы это здорово


Что вы можете делать, если вы умеете писать компилятор? Быть может, вам захочется написать свою новую версию JavaScript на испанском… Как насчет EspañolScript?
// ES (español script)
función () {
  si (verdadero) {
    return «¡Hola!»
  }
}

Уже есть те, кто написали свой язык, используя Emoji (Emojicode) или цветные изображения (Piet). Возможности безграничны!

Обучение в процессе создания компилятора


Создавать компилятор было весело, и я многому научилась, касаемо разработки ПО. Перечислю всего несколько вещей, которым я научилась в процессе написания компилятора.

1. Вполне нормально чего-то не знать


Как и нашему лексическому анализатору, вам не нужно все знать с самого начала. Если вы не совсем понимаете часть какого-то кода или технологии, это нормально перенести работу с этим на следующий шаг. Не стоит нервничать, рано или поздно вы разберетесь в этом!

2. Уделите внимание тексту ваших сообщений об ошибках


Роль парсера — четко следовать инструкциям и проверять, написано ли все так, как сказано в правилах. Да, ошибки случаются часто. Когда это происходит, попробуйте отправлять информативное, дружеское сообщение об ошибке. Легко сказать — “Так оно не работает” (“ILLEGAL Token” или “undefined is not a function” в JavaScript), но вместо этого попробуйте предоставить пользователю как можно больше полезной информации.

Это также относится к командной коммуникации. Когда кто-то застрял со своим таском, вместо того, чтобы сказать ”Да это не работает”, можно начать говорить “Я бы поискал информацию в google по таким ключевым словам как…” или “Я рекомендую почитать такую-то документацию”. Вам не нужно брать на себя работу другого человека, но вы определенно можете помочь ему сделать его работу лучше и быстрее всего лишь подкинув ему свежую идею.

Язык программирования Elm использует этот подход в выводе сообщений об ошибках, где пользователю предлагаются варианты решения его проблемы (“Maybe you want to try this?”).

3. Контекст наше все


И наконец, точно так же, как наш трансформатор превратил один тип AST в другой, более подходящий для конечного результата, все в программировании зависит от контекста.

Нет ни одного совершенно идеального решения. Не делайте что-то, только потому, что сейчас это модно, или потому, что вы уже делали это раньше, подумайте сначала о контексте задачи. Вещи, которые работают для одного пользователя, могут быть совершенно ужасны для другого.

Поэтому цените работу “трансформаторов”, вы вполне возможно знаете такого в своей команде, человека, который хорошо завершает, начатую кем-то работу, или делает рефакторинг. Он по сути не создает новый код, но ведь результат его работы чертовски важен для конечного качественного продукта.

Надеюсь, вам понравилась эта статья и что я убедила вас, как это здорово писать компиляторы и самому быть компилятором!

Это перевод статьи Mariko Kosaka, которая является частью ее выступления на JSConf Colombia 2016 в Медельине, Колумбия. Если вы хотите узнать больше об этом, вы можете найти слайды здесь и оригинал статьи здесь.

JavaScript, V8, некоторые приёмы разработки / RUVDS.com corporate blog / Habr

Публикуя первую часть перевода этого руководства по Node.js, мы решили узнать мнение аудитории о том, стоит ли нам продолжать работу над проектом, и провели небольшой опрос. Как оказалось, нашу инициативу поддержали примерно 94% проголосовавших. Поэтому встречайте вторую часть руководства по Node.js.

Сегодня мы поговорим о том, какими знаниями в области JS нужно обладать для того, чтобы продуктивно разрабатывать приложения для платформы Node.js, обсудим различия браузерного и серверного JavaScript-кода, поговорим о JS-движках и о некоторых приёмах Node.js-разработки.

[Советуем почитать] Другие части циклаЧасть 1: Общие сведения и начало работы
Часть 2: JavaScript, V8, некоторые приёмы разработки
Часть 3: Хостинг, REPL, работа с консолью, модули
Часть 4: npm, файлы package.json и package-lock.json
Часть 5: npm и npx
Часть 6: цикл событий, стек вызовов, таймеры
Часть 7: асинхронное программирование
Часть 8: Руководство по Node.js, часть 8: протоколы HTTP и WebSocket
Часть 9: Руководство по Node.js, часть 9: работа с файловой системой
Часть 10: Руководство по Node.js, часть 10: стандартные модули, потоки, базы данных, NODE_ENV
Полная PDF-версия руководства по Node.js

Какими JS-знаниями нужно обладать для Node.js-разработки?


Предположим, вы только начали заниматься программированием. Насколько глубоко вам нужно изучить JavaScript для успешного освоения Node.js? Начинающему трудно достичь такого уровня, когда он приобретёт достаточную уверенность в своих профессиональных навыках. К тому же, изучая программирование, вы можете почувствовать, что не понимаете точно, где заканчивается браузерный JavaScript и начинается разработка для Node.js.

Если вы находитесь в самом начале пути JavaScript-программиста, я посоветовал бы вам, прежде чем писать для Node.js, хорошо освоить следующие концепции языка:

  • Лексические конструкции.
  • Выражения.
  • Типы.
  • Переменные.
  • Функции.
  • Ключевое слово this.
  • Стрелочные функции
  • Циклы
  • Области видимости.
  • Массивы.
  • Шаблонные строки.
  • Применение точки с запятой.
  • Работа в строгом режиме.

На самом деле, этот список можно продолжать, но если вы всё это освоите, это значит, что вы заложите хорошую базу для продуктивной клиентской и серверной разработки на JavaScript.
Следующие концепции языка, кроме того, весьма важны для понимания идей асинхронного программирования, которые являются одной из базовых частей Node.js. В частности, речь идёт о следующем:
  • Асинхронное программирование и функции обратного вызова.
  • Таймеры.
  • Промисы.
  • Конструкция async/await.
  • Замыкания.
  • Цикл событий.

Существует множество материалов по JavaScript, которые позволяют начинающим освоить язык. Например, вот учебный курс автора данного руководства, вот весьма полезный раздел MDN, вот учебник сайта javascript.ru. Изучить базовые механизмы JavaScript можно на freecodecamp.com.

Выше мы говорили о том, что начинающих может беспокоить вопрос о том, где проходит граница между серверной и клиентской разработкой на JavaScript. Поговорим об этом.

Различия между платформой Node.js и браузером


Чем JS-разработка для Node.js отличается от браузерного программирования? Сходство между этими средами заключается в том, что и там и там используется один и тот же язык. Но разработка приложений, рассчитанных на выполнение в браузере, очень сильно отличается от разработки серверных приложений. Несмотря на использование одного и того же языка, существуют некоторые ключевые различия, которые и превращают два эти вида разработки в совершенно разные занятия.

Надо отметить, что если тот, кто раньше занимался фронтендом, начинает изучать Node.js, у него имеется весьма серьёзная возможность довольно быстро освоить всё что нужно благодаря тому, что писать он будет на уже знакомом ему языке. Если к необходимости освоить новую среду добавляется ещё и необходимость изучить новый язык, задача значительно усложняется.
Итак главное различие между клиентом и сервером заключается в окружении, для которого приходится программировать, в экосистемах этих сред.

В браузере основной объём работы приходится на выполнение различных операций с веб-документами посредством DOM, а также — на использование других API веб-платформы, таких, скажем, как механизмы для работы с куки-файлами. Всего этого в Node.js, конечно, нет. Тут нет ни объекта document, ни объекта window, равно как и других объектов, предоставляемых браузером.

В браузере, в свою очередь, нет тех программных механизмов, которые имеются в среде Node.js и существуют в виде модулей, которые можно подключать к приложению. Например, это API для доступа к файловой системе.

Ещё одно различие между клиентской и серверной разработкой на JS заключается в том, что при работе в среде Node.js разработчик полностью контролирует окружение. Если только вы не занимаетесь разработкой опенсорсного приложения, которое может быть запущено где угодно, вы точно знаете, например, на какой версии Node.js будет работать ваш проект. Это очень удобно в сравнении с клиентским окружением, где вашему коду приходится работать в имеющемся у пользователя браузере. Кроме того, это означает, что вы можете, не опасаясь проблем, пользоваться новейшими возможностями языка.

Так как JavaScript крайне быстро развивается, браузеры просто не успевают достаточно оперативно реализовать все его новшества. К тому же, далеко не все пользователи работают на самых свежих версиях браузеров. В результате разработчики, которые хотят использовать в своих программах что-то новое, вынуждены это учитывать, заботиться о совместимости их приложений с используемыми браузерами, что может вылиться в необходимость отказа от современных возможностей JavaScript. Можно, конечно, для преобразования кода в формат, совместимый со стандартом EcmaScript 5, который поддерживают все браузеры, воспользоваться транспилятором Babel, но при работе с Node.js вам это не понадобится.

Ещё одно различие между Node.js и браузерами заключается в том, что в Node.js используется система модулей CommonJS, в то время как в браузерах можно наблюдать начало реализации стандарта ES Modules. На практике это означает, что в настоящее время в Node.js, для подключения внешнего кода, используется конструкция require(), а в браузерном коде — import.

V8 и другие JavaScript-движки


V8 — это название JavaScript-движка, используемого в браузере Google Chrome. Именно он отвечает за выполнение JavaScript-кода, который попадает в браузер при работе в интернете. V8 предоставляет среду выполнения для JavaScript. DOM и другие API веб-платформы предоставляются браузером.

JS-движок независим от браузера, в котором он работает. Именно этот факт сделал возможным появление и развитие платформы Node.js. V8 был выбран в качестве движка для Node.js в 2009 году. В результате прямо-таки взрывного роста популярности Node.js V8 оказался движком, который в наши дни отвечает за выполнение огромного количества серверного JS-кода.

Экосистема Node.js огромна. Благодаря этому V8 также используется, посредством проектов наподобие Electron, при разработке настольных приложений.

Надо отметить, что, помимо V8, существуют и другие движки:

  • В браузере Firefox применяется движок SpiderMonkey.
  • В Safari применяется JavaScriptCore (он ещё называется Nitro).
  • В Edge используется движок Chakra.

Список JS-движков этим не ограничивается.

Эти движки реализуют спецификацию ECMA-262, называемую ещё ECMAScript. Именно эта спецификация стандартизирует JavaScript. Свежую версию стандарта можно найти здесь.

▍Разработка JS-движков и стремление к производительности


Движок V8 написан на C++, его постоянно улучшают. Он может выполняться на многих системах, в частности, на Mac, Windows и Linux. Здесь мы не будем говорить о деталях реализации V8. Сведения о них можно найти в других публикациях, в том числе — на официальном сайте V8. Они со временем меняются, иногда — очень серьёзно.

V8 постоянно развивается, то же самое можно сказать и о других движках. Это приводит, в частности, к росту производительности веб-браузеров и платформы Node.js. Производители движков для браузеров постоянно соревнуются, борясь за скорость выполнения кода, продолжается это уже многие годы. Всё это идёт на пользу пользователям и программистам.

▍Интерпретация и компиляция


JavaScript считается интерпретируемым языком, но современные движки занимаются далеко не только интерпретацией JS-кода. Они его компилируют. Это веяние можно наблюдать с 2009-го года, когда компилятор JavaScript был добавлен в Firefox 3.5, после чего и другие производители движков и браузеров переняли эту идею.

V8 выполняет компиляцию JavaScript для повышения производительности кода. Со времени появления Google Maps в 2004-м году JavaScript эволюционировал, превратился из языка, на котором, для реализации интерактивных возможностей веб-приложений, обычно писали по несколько десятков строк, в язык, на котором пишут браузерные приложения, состоящие из тысяч или даже сотен тысяч строк кода. Такие приложения могут выполняться в браузере часами, что серьёзно отличается от старых сценариев использования JS, код на котором, например, мог применяться лишь для проверки правильности данных, вводимых в формы. В современных условиях компиляция кода имеет огромный смысл, так как, хотя выполнение этого шага может отложить момент запуска кода, после компиляции код оказывается гораздо более производительным, чем тот, который обрабатывался бы исключительно интерпретатором и запускался бы быстрее, но работал бы медленнее.

Теперь, обсудив некоторые положения, касающиеся JS-движков, интерпретации и компиляции кода, перейдём к практике. А именно, поговорим о том, как завершать работу Node.js-приложений.

Выход из Node.js-приложения


Существует несколько способов завершения работы Node.js-приложений.

Так, при выполнении программы в консоли, завершить её работу можно, воспользовавшись сочетанием клавиш ctrl+c. Но нас больше интересуют программные способы завершения работы приложений. И начнём мы, пожалуй, с самой грубой команды выхода из программы, которую, как вы сейчас поймёте, лучше не использовать.

Модуль ядра process предоставляет удобный метод, который позволяет осуществить программный выход из Node.js-приложения. Выглядит это так:

process.exit()

Когда Node.js встречает в коде такую команду, это приводит к тому, что его процесс мгновенно завершается. Это означает, что абсолютно всё, чем занималась программа, будет довольно грубо и безусловно прервано. Речь идёт о невызванных коллбэках, о выполняемых в момент выхода сетевых запросах, о действиях с файлами, об операциях записи в stdout или stderr.

Если вас такое положение дел устраивает — можете этим методом пользоваться. При его вызове можно передать ему целое число, которое будет воспринято операционной системой как код выхода из программы.

process.exit(1)

По умолчанию этот код имеет значение 0, что означает успешное завершение работы. Другие коды выхода имеют другие значения, которые могут оказаться полезными для использования их в собственной системе для того, чтобы наладить взаимодействие одних программ с другими.
Подробности о кодах завершения работы программ можно почитать здесь.

Код выхода, кроме того, можно назначить свойству process.exitCode. Выглядит это так:

process.exitCode = 1

После того, как программа завершит работу, Node.js вернёт системе этот код.

Надо отметить, что работа программы самостоятельно завершится естественным образом после того, как она выполнит все заданные в ней действия. Однако в случае с Node.js часто встречаются программы, которые, в идеальных условиях, рассчитаны на работу неопределённой длительности. Речь идёт, например, об HTTP-серверах, подобных такому:

const express = require('express')
const app = express()
app.get('/', (req, res) => {
  res.send('Hi!')
})
app.listen(3000, () => console.log('Server ready'))

Такая программа, если ничего не случится, в теории, может работать вечно. При этом, если вызвать process.exit(), выполняемые ей в момент вызова этой команды операции будут прерваны. А это плохо.

Для завершения работы подобных программ нужно воспользоваться сигналом SIGTERM и выполнить необходимые действия с помощью соответствующего обработчика.

Обратите внимание на то, что для использования объекта process не нужно ничего подключать с помощью require, так как этот объект доступен Node.js-приложениям по умолчанию.

Рассмотрим следующий пример:

const express = require('express')
const app = express()
app.get('/', (req, res) => {
  res.send('Hi!')
})
app.listen(3000, () => console.log('Server ready'))
process.on('SIGTERM', () => {
  app.close(() => {
    console.log('Process terminated')
  })
})

Что такое «сигналы»? Сигналы — это средства взаимодействия процессов в стандарте POSIX (Portable Operating System Interface). Они представляют собой уведомления, отправляемые процессу для того, чтобы сообщить ему о неких событиях.

Например, сигнал SIGKILL сообщает процессу о том, что ему нужно немедленно завершить работу. Он, в идеале, работает так же, как process.exit().

Сигнал SIGTERM сообщает процессу о том, что ему нужно осуществить процедуру нормального завершения работы. Подобные сигналы отправляются из менеджеров процессов, вроде upstart или supervisord, и из многих других.

Отправить такой сигнал можно и из самой программы, воспользовавшись следующей командой:

process.kill(process.pid, 'SIGTERM')

Для успешного выполнения подобной команды нужно знать PID процесса, который планируется завершить.

Чтение переменных окружения из Node.js


Модуль ядра process имеет свойство env, которое даёт доступ ко всем переменным окружения, которые были заданы на момент запуска процесса.

Вот пример работы с переменной окружения NODE_ENV, которая, по умолчанию, установлена в значение development:

process.env.NODE_ENV // "development"

Если, до запуска скрипта, установить её в значение production, это сообщит Node.js о том, что программа выполняется в продакшн-окружении.

Подобным образом можно работать и с другими переменными среды, например, с теми, которые установлены вами самостоятельно.

Итоги


Сегодня мы коснулись вопросов браузерного и серверного программирования на JavaScript, поговорили о JS-движках, о том, как завершать работу серверных приложений, и о том, как читать переменные среды из Node.js-программ. В следующий раз мы расскажем о хостингах для Node.js-приложений, о том, как пользоваться Node.js в режиме REPL, о работе с аргументами, которые можно передавать скриптам при их вызове, о взаимодействии с консолью, и об оформлении кода в виде модулей.

Уважаемые читатели! Какие учебные материалы по JavaScript вы посоветовали бы начинающим?

Введение

Google Closure Compiler — уникальный инструмент, разработанный Google для сжатия и обфускации собственного javascript.

У него есть ряд интересных особенностей, которые отличают его от прочих упаковщиков.

Вместе с этим, инструмент это достаточно сложный. Основные его фичи скрываются в продвинутом режиме сжатия, для применения которого нужны соответствующие навыки и стиль программирования.

Google Closure Compiler написан на языке Java, причем так, что его достаточно просто расширить — конечно, если вам знакомы понятия компилятора, синтаксического дерева и понятен Java.

Официальная документация находится на http://code.google.com/intl/ru/closure/compiler/docs/overview.html.

На момент написания этой статьи, она, как и сам компилятор, еще в процессе разработки. Во всяком случае, такие слова я слышал от разработчиков компилятора, да и общее состояние текстов и кода на это похоже.

В этом введении мы разберем основные опции компилятора и осуществим компиляцию простых примеров.

Существует 3 способа запуска компилятора:

  1. Через веб-интерфейс. То есть, зайти на страничку, ввести или загрузить код и скомпилировать его с нужными опциями. Результат появится в текстовом поле.

    Этот способ удобен, чтобы поиграться с оптимизациями, понять от чего чего бывает, не более.

    Документация по этому способу: http://code.google.com/intl/ru/closure/compiler/docs/gettingstarted_ui.html.

  2. Через веб-сервис. Этот способ подразумевает отправку POST-запроса на адрес http://closure-compiler.appspot.com/compile. В параметрах запроса описаны флаги компиляции и передан код.
    Ответом будет сжатый javascript, либо ошибки компиляции.
    Этот способ удобен тем, что на веб-сервис google выкладывает, как правило, последнее стабильное API. Это убирает необходимость в хранении и обновлении компилятора.
    Документация по этому способу находится на

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *