Выразительный JavaScript.

Разбор (parsing).

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

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

Для упрощения парсинга строки в Egg не будут поддерживать обратных слэшей и подобных вещей. Строка – просто последовательность символов, не являющихся двойными кавычками, заключённая в двойные кавычки. Число – последовательность цифр. Имена переменных могут состоять из любых символов, не являющихся пробелами и не имеющих специального значения в синтаксисе.

Приложения записываются так же, как в JavaScript — при помощи скобок после выражения и с любым количеством аргументов в скобках, разделённых запятыми.

Do(define(x, 10),

If(>(x, 5)),

Print("много"),

Print("мало")).

Однородность языка означает, что то, что в JavaScript является операторами (например, >), применяется так же, как и остальные функции. Так как в синтаксисе нет концепции блоков, нам нужна конструкция do для обозначения нескольких вещей, выполняемых последовательно.

Структура данных, описывающая программу, будет состоять из объектов выражений, у каждого из которых будет свойство type, отражающее тип этого выражения и другие свойства, описывающие содержимое.

Выражения типа “value” представляют строки или числа. Их свойство value содержит строку или число, которое они представляют. Выражения типа “word” используются для идентификаторов (имён). У таких объектов есть свойство name, содержащее имя идентификатора в виде строки. И наконец, выражения “apply” представляют приложения. У них есть свойство operator, ссылающееся на применяемое выражение, и свойство args с массивом аргументов.

Часть >(x, 5) будет представлена так:

{

Type: "apply",

Operator: {type: "word", name: ">"},

Args: [

{type: "word", name: "x"},

{type: "value", value: 5}.

]

}

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

Выразительный JavaScript.

Структура синтаксического дерева.

Сравните это с парсером, написанным нами для файла настроек в главе 9, у которого была простая структура: он делил ввод на строки и обрабатывал их одну за другой. Там было всего несколько форм, которые разрешено принимать строке.

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

Мы определяем функцию parseExpression, принимающую строку на вход и возвращающую объект, содержащий структуру данных для выражения с начала строки, вместе с частью строки, оставшейся после парсинга. При разборе подвыражений (таких, как аргумент приложения), эта функция снова вызывается, возвращая выражение аргумента вместе с оставшимся текстом. Тот текст может, в свою очередь, содержать ещё аргументы, или же быть закрывающей скобкой, завершающей список аргументов.

Первая часть парсера:

Function parseExpression(program) {

Program = skipSpace(program);

Var match, expr;

If (match = /^"([^"]*)"/.exec(program)).

Expr = {type: "value", value: match[1]};

Else if (match = /^\d+\b/.exec(program)).

Expr = {type: "value", value: Number(match[0])};

Else if (match = /^[^\s(),"]+/.exec(program)).

Expr = {type: "word", name: match[0]};

Else.

Throw new SyntaxError("Неожиданный синтаксис: " + program);

Return parseApply(expr, program.slice(match[0].length));

}

Function skipSpace(string) {

Var first = string.search(/\S/);

If (first == -1) return "";

Return string.slice(first);

}

Поскольку Egg разрешает любое количество пробелов в элементах, нам надо постоянно вырезать пробелы с начала строки. С этим справляется skipSpace.

Пропустив начальные пробелы, parseExpression использует три регулярки для распознавания трёх простых (атомарных) элементов, поддерживаемых языком: строк, чисел и слов. Парсер создаёт разные структуры для разных типов. Если ввод не подходит ни под одну из форм, это не является допустимым выражением, и он выбрасывает ошибку. SyntaxError – стандартный объект для ошибок, который создаётся при попытке запуска некорректной программы JavaScript.

Мы можем отрезать обработанную часть программы, и передать его, вместе с объектом выражения, в parseApply, определяющая, не является ли выражение приложением. Если так и есть, он парсит список аргументов в скобках.

Function parseApply(expr, program) {

Program = skipSpace(program);

If (program[0] != "(").

Return {expr: expr, rest: program};

Program = skipSpace(program.slice(1));

Expr = {type: "apply", operator: expr, args: []};

While (program[0] != ")") {

Var arg = parseExpression(program);

Expr.args.push(arg.expr);

Program = skipSpace(arg.rest);

If (program[0] == ",").

Program = skipSpace(program.slice(1));

Else if (program[0] != ")").

Throw new SyntaxError("Ожидается ',' or ')'");

}

Return parseApply(expr, program.slice(1));

}

Если следующий символ программы – не открывающая скобка, то это не приложение, и parseApply просто возвращает данное ей выражение.

В ином случае, она пропускает открывающую скобку и создаёт объект синтаксического дерева для этого выражения. Затем она рекурсивно вызывает parseExpression для разбора каждого аргумента, пока не встретит закрывающую скобку. Рекурсия непрямая, parseApply и parseExpression вызывают друг друга.

Поскольку приложение само по себе может быть выражением ( multiplier(2)(1)), parseApply должна, после разбора приложения, вызвать себя снова, проверив, не идёт ли далее другая пара скобок.

Вот и всё, что нам нужно для разбора Egg. Мы обернём это в удобную функцию parse, проверяющую, что она дошла до конца строки после разбора выражения (программа Egg – это одно выражение), и это даст нам структуру данных программы.

Function parse(program) {

Var result = parseExpression(program);

If (skipSpace(result.rest).length > 0).

Throw new SyntaxError("Неожиданный текст после программы");

Return result.expr;

}

Console.log(parse("+(a, 10)"));

// → {type: "apply",

// operator: {type: "word", name: "+"},

// args: [{type: "word", name: "a"},

// {type: "value", value: 10}]}.

Работает! Она не выдаёт полезной информации при ошибке, и не хранит номера строки и столбца, с которых начинается каждое выражение, что могло бы пригодиться при разборе ошибок – но для нас и этого хватит.