Статья-туториал для тех, кто хочет узнать, как из заголовка получается «6» методом рекурсивного спуска. Начнём с самого простого и дойдём до вычисления -1.82 или около того из строки -2.1+ .355 / (cos(pi % 3) + sin(0.311)). Конечно, этот метод неоднократно описан на Хабре и зачитан каждому айтишному первокурснику. В своей версии я хочу изложить его очень просто и подробно, с позиции элементарной практики на JavaScript. Ссылки на рабочий код — в самом низу. Разберём невыносимо простой пример: на входе у нас строка с двойкой, а на выходе должно быть число 2. Разумеется, тут достаточно вызова функции parseInt, но давайте напишем так, чтобы было от чего отталкиваться на следующих уровнях. Зачем мы делим строку на символы, да ещё когда она состоит из всего одного? На самом деле нам нужны не отдельные символы, а элементы выражения, которые называются токенами. Числа — это один вид токенов, операторы +, -, *, / — другой (причём каждый оператор может быть отдельным видом), скобки — третий и т.д. Но для начала пускай каждый токен будет символом, ведь так выражение гораздо проще разрезать на токены (токенизировать): достаточно вызвать .split(''). Кроме того, тут появляется понятие EOF (end of file) и вводится правило, по которому после выражения сразу должен идти конец файла/ввода. Представьте, что на вход подаётся строка 2#. Без проверки EOF мы бы так и вывели двойку, проигнорировав #, а такие вольности недопустимы. Выражение 2+2 точно так же должно сбить с толку наш парсер на этом этапе. Кстати, в JavaScript метод shift() выдаёт первый элемент и удаляет его из массива. Этим будем пользоваться, чтобы идти по токенам. Иногда будем проверять токен, прежде чем удалить его, типа if (tokens[0] === '+') { tokens.shift(); ... }. Пойдём дальше по выражению дальше и, встречая + либо - , будем решать, что делать со следующей за ними цифрой — добавить или вычесть из накопленного результата. Кроме того, мы вынесли часть алгоритма в отдельную функцию expr, хоть и можно было продолжать писать всё в calc. Такое разделение пригодится нам позже. Давайте подсунем нашему калькулярсеру какую-нибудь ерунду, убедимся, что он не выводит случайный числовой результат, и двинемся дальше. Пока сойдёт. Одинокий плюс никакого числа не представляет, а поддержку +1 (унарный плюс) мы добавим позже вместе с унарным минусом. Естественно, тут подвох в приоритете операций умножения/деления над операциями сложения/вычитания, поэтому нельзя просто добавить блок else if (tokens[0] === '*') {...} вслед за плюсом и минусом, ведь тогда получится 8. Здесь-то на помощь и приходит рекурсия языка программирования, на котором мы пишем этот калькулятор. В JavaScript рекурсия есть, и потому этот метод легко реализуется. Но есть условно современные языки без поддержки рекурсии — например, structured text в IEC 61 131–3, на котором программируются контроллеры в среде промышленной автоматики. Оставим двойки для красивого заголовка, и, чтобы проще было следить за руками, возьмём пример 3+4×5. Да, говоря обывательским языком, нам нужно сначала посчитать 4×5 и уже потом складывать это с тройкой. Но можно переформулировать и более удобным для программирования образом: сложить тройку с тем, что главнее сложения. А значит, можно как обычно начать считывать тройку, потом + за ней, а потом «отвлечься» на вычисление 4×5, результат которого и сложить с ранее замеченной тройкой. Вот на этом «отвлечься» и строится принцип рекурсивного спуска. Давайте оформим «отвлечься» новой функцией term, которую реализуем точно так же, как сделана expr выше. Ну а в expr будем вызывать term там, где прежде считывали цифры. Как видите, мы сразу заумели делать даже 3/2+4*5. Что если всё-таки нужна восьмёрка при помощи скобок? Значит, нужно в term отвлечься на то, что заключено в скобки. А в скобках может быть то, что должно обрабатываться функцией expr , для чего мы её и выделили некоторое время назад. Очередное отвлечение назовём factor, из которой либо вызовем expr между скобками (круг замкнулся), либо считаем число. В принципе, на этом заканчивается основа рекурсивного спуска. Дальше будем прокачивать эту структуру. До сих пор мы парсили массив символов, состоящий из цифр, бинарных операторов и круглых скобок. Каждый символ был токеном, и это очень упрощало нам жизнь. Но дальше развивать это будет сложно. Давайте теперь посимвольный split('') заменим чуть более взрослым лексическим разбором (по большому счёту это синоним токенизации). У нас будет несколько регулярных выражений по всем типам токенов, и мы будем пытаться отрезать от входной строки по кусочку, который соответствует хоть одной регулярке. Сначала упрощённый пример, напрямую не связанный с нашей задачей: Тут важно отметить ^ в начале каждого регулярного выражения, означающее поиск соответствия строго с начала строки. Вместе со slice() это даёт тот самый проход по строке «кусочками».  Такой лексер на регулярках очень компактен и читабелен, но его гибкость и производительность весьма ограничены для по-настоящему взрослого лексического разбора; имейте это в виду, когда пойдёте делать свой язык программирования. Тем не менее, современные рантаймы JS оптимизируют разрезание строк через slice, по сути сводя его к манипуляциям с указателями. В сумме с простыми регулярками, прибитыми к началу строки, это должно давать неплохую производительность. Давайте теперь растянем такой лексер на лексику наших выражений. Вместо строк для обозначения типов возьмём JS-овский Symbol и упакуем матчинг внутрь функции-генератора (это такие функции, которые могут выдавать по несколько значений за один вызов; в JavaScript они обозначаются звёздочкой и «выплёвывают» значения через yield). Не знаю, заметили вы сами или нет, но здесь мы впервые разобрали выражение (2+2)×2 - обратите внимание на знак умножения, это не звёздочка, причём воспринимается наравне со звёздочкой. Там есть ещё один вариант умножения, а также «типографский» минус. Пускай это будет фишкой нашего языка для упоротых по типографике эстетов. В сущности, это то же самое, что split(''), только кусочки покрупнее. Среди прочего у нас теперь есть явное обозначение пробелов (whitespace), которые мы легко можем пропустить, и end-of-file в виде отдельного токена. Но дело не ограничивалось одним лишь split(''), правда? Мы подглядывали следующий токен через if (tokens[0] === '*'), а также «проглатывали» токены через shift(). Кое-где мы даже убеждались, что токен именно такой, а не другой, выбрасывая ошибку 'missing closing parenthesis'. Воспроизведём эти удобства в классе: Это джентельменский набор для простейшего парсинга: метод next() — эквивалент shift() из массива с автоматическим пропуском пробелов; метод accept(type) — эквивалент проверки с последующим удалением из массива; заодно возвращает текст совпадения; метод expect(type) — эквивалент проверки с выбрасыванием ошибки при несовпадении; метод error(err) — выбрасывание ошибки с индикацией текущей позиции. То есть всё это у нас было, но прямо по месту парсинга, "inline". Давайте теперь адаптируем функции парсинга/вычисления как методы того класса: Пробуем: Ещё у нас появилась операция получения остатка от деления (оператор %) и числа дробные поддерживаются, т.к. вместо parseInt делаем parseFloat. Как бы нам добавить сюда унарный плюс и унарный минус? По первой интуиции это знак рядом с числовым литералом, который можно учесть подобно скобкам в factor. Но это обманчивая интуиция. Возможны такие выражения: -(2+3), -sin(0.33). Стало быть, это отдельная вещь c отдельной рекурсией. Именованные операнды типа "pi" чем-то напоминают числовые литералы, не правда ли? По крайней мере они стоят там же относительно операторов. Ну, например, 2 + -(5 * 3) vs 2 + -(pi * 3). Похоже? И несложно представить себе какие-нибудь римские числа, которые вообще не отличаются от переменных: 10 + 21 = 10 + XXI. Только с функциями какая-то фигня: у них свои скобки. Значит, и будем обрабатывать всё там же, где числа — в factor. На что тут можно обратить внимание? Во-первых, мы не пытаемся как-либо реюзать обработку приоритезирующих скобок; здесь скобки — это отдельная синтаксическая конструкция. Если бы мы поддерживали несколько аргументов, то прошлись бы здесь по ним через запятую. Во-вторых, что мы реюзаем, так это функции Math из JavaScript. Можно сказать, что переменные, которые вы определяете в этом блоке, образуют окружение, в котором вычисляется введённая вами формула (ну или «исполняется ваш скрипт» если бы мы писали интерпретатор какого‑нибудь языка). Как обещано. В текущем виде лексер вообще не способен его обрабатывать. Чтобы избежать падения на каком-нибудь там }@5+1, можно пропускать по одному символу, пока совпадения вновь не появятся, и что с ними дальше делать, уже зависит от применения; в общем случае надо показать ошибку и прекратить вычисление. Хорошим тоном будет накопить неизвестные символы и выдать их одной ошибкой. Целью заметки было показать «на пальцах», как работает и из чего строится простейший рекурсивный парсер-калькулятор арифметических выражений, не погружаясь в термины грамматик языков программирования. Подобные парсеры можно встретить под капотом софта, который на уровне пользователя позволяет вводить формулы/выражения, например для срабатывания алертов или для формирования графиков. Встречаются и другие подходы к обработке выражений: перевод в постфиксную форму с последующим вычислением, применение генераторов парсеров вместо ручного их написания. Многие языки и среды исполнения предлагают готовые решения, чтобы вычислять пользовательские выражения. S: jdoodle.com/ia/Kmu XXL: jdoodle.com/ia/Kmt