ручное кодирование парсера - PullRequest
26 голосов
/ 13 апреля 2010

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

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

Возможно, это будет серия вопросов, но сейчас я начинаю с лексера. Правила токенизации для CSS можно найти здесь .

Мне кажется, что я сам пишу код (надеюсь, вы можете сделать вывод об остальном из этого фрагмента):

public CssToken ReadNext()
{
    int val;
    while ((val = _reader.Read()) != -1)
    {
        var c = (char)val;
        switch (_stack.Top)
        {
            case ParserState.Init:
                if (c == ' ')
                {
                    continue; // ignore
                }
                else if (c == '.')
                {
                    _stack.Transition(ParserState.SubIdent, ParserState.Init);
                }
                break;

            case ParserState.SubIdent:
                if (c == '-')
                {
                    _token.Append(c);
                }
                _stack.Transition(ParserState.SubNMBegin);
                break;

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

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

Я бы очень хотел подумать о написании многократно используемых фрагментов кода. Этот метод Transition в настоящее время предназначен для этого, он вытолкнет текущее состояние стека и затем передаст аргументы в обратном порядке. Таким образом, когда я пишу Transition(ParserState.SubIdent, ParserState.Init), он "вызовет" подпрограмму SubIdent, которая после завершения вернется в состояние Init.

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

Ответы [ 5 ]

28 голосов
/ 14 апреля 2010

То, что вы пишете, называется автоматом. Обычно это больше, чем нужно для написания лексера, и, конечно, чрезмерно, если вы пишете лексер для современного языка, такого как CSS. Синтаксический анализатор с рекурсивным спуском близок к автомату с нажатием кнопки, но синтаксические анализаторы с рекурсивным спуском гораздо проще писать и понимать. Большинство генераторов синтаксических анализаторов генерируют автоматы нажатия.

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

Для этого есть инструменты. Инструмент lex и его потомки хорошо известны и переведены на многие языки. У ANTLR toolchain также есть компонент лексера. Мой предпочтительный инструмент - ragel на платформах, которые его поддерживают. Писать лексер вручную в большинстве случаев времени практически невозможно, и код, сгенерированный этими инструментами, вероятно, будет быстрее и надежнее.

Если вы хотите написать свой собственный лексер от руки, хорошие часто выглядят примерно так:

function readToken() // note: returns only one token each time
    while !eof
        c = peekChar()
        if c in A-Za-z
            return readIdentifier()
        else if c in 0-9
            return readInteger()
        else if c in ' \n\r\t\v\f'
            nextChar()
        ...
    return EOF

function readIdentifier()
    ident = ""
    while !eof
        c = nextChar()
        if c in A-Za-z0-9
            ident.append(c)
        else
            return Token(Identifier, ident)
            // or maybe...
            return Identifier(ident)

Тогда вы можете написать свой парсер как парсер рекурсивного спуска. Не пытайтесь объединить этапы лексера / парсера в один, это приводит к полному беспорядку кода. (По словам автора Parsec, он тоже медленнее).

5 голосов
/ 14 апреля 2010

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

Некоторые вещи, которые я бы порекомендовал получить ручку
1. Хороший дизайн для вашего сканера / лексера, это то, что будет маркировать ваш исходный код для вашего парсера.
2. Следующая вещь - парсер, если у вас есть хороший ebnf для исходного языка, парсер обычно может довольно неплохо перевести в рекурсивный приличный парсер.
3. Другая структура данных, которая вам действительно понадобится, - это таблица символов. Это может быть как простая хеш-таблица, так и сложная, как древовидная структура, которая может представлять сложные структуры записей и т. Д. Я думаю, что для CSS вы можете оказаться где-то посередине.
4. И, наконец, вы хотите заняться генерацией кода. У вас есть много вариантов здесь. Для переводчика вы можете просто интерпретировать на лету, когда вы анализируете код. Лучшим подходом может быть создание for i-кода, для которого вы затем можете написать iterpreter, а затем даже компилятор. Конечно, для платформы .NET вы можете напрямую сгенерировать IL (вероятно, не применимо для CSS :))


Для справки, я полагаю, вы не вникаете в глубокую теорию, и я не виню вас. Действительно хорошей отправной точкой для получения основ без сложного кода, если вы не возражаете против Паскаля, который является Джеком Креншоу, «Давайте построим компилятор»
http://compilers.iecc.com/crenshaw/
Удачи, я уверен, что вам понравится этот проект.

4 голосов
/ 13 апреля 2010

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

В shift-Reduce термин «Reduce» относится к операции, выполняемой с явно поддерживаемым стеком токенов. Например, если вершина стека стала Term, Operator, Term, то можно применить правило сокращения, в результате чего Expression заменит шаблон. Правила сокращения явно закодированы в структуре данных, используемой анализатором сдвига-уменьшения; в результате все правила сокращения можно найти в одном месте исходного кода.

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

В частности, поскольку синтаксический анализатор сдвига-уменьшения имеет явное кодирование правил для выполнения «сокращений», анализатор легко расширяется, чтобы сформулировать, какие виды токенов могли бы юридически следовать. (например, "; ожидается"). Реализация рекурсивного спуска не может быть легко расширена, чтобы сделать то же самое.

Великой книгой по обоим видам синтаксического анализатора и компромиссам в реализации различных видов сдвига-уменьшения является «Введение в конструирование компиляторов», Томас У. Парсонс .

Сдвиг-уменьшение иногда называется "восходящим" разбором, а рекурсивный спуск иногда называется "нисходящим" разбором. В используемой аналогии узлы, составленные с наивысшим приоритетом (например, «факторы» в выражении умножения), считаются «в нижней части» синтаксического анализа. Это согласуется с аналогией, используемой в «спуске» «рекурсивного спуска».

4 голосов
/ 13 апреля 2010

Вам нужно написать свой парсер рекурсивного спуска из вашего BNF / EBNF. Недавно мне пришлось написать свое, и эта страница очень помогла. Я не уверен, что вы подразумеваете под "просто кодом". Вы хотите знать, как написать свой собственный рекурсивный парсер?

Если вы хотите это сделать, вам нужно сначала установить грамматику. Если у вас есть EBNF / BNF, парсер может быть легко записан с него.

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

UPDATE

Исходя из вашего комментария, мне пришлось написать анализатор с рекурсивным спуском в Javascript с нуля. Вы можете взглянуть на парсер здесь . Просто найдите функцию constraints. Я написал свою собственную tokenize функцию для токенизации ввода. Я также написал другую вспомогательную функцию (peek, о которой я упоминал ранее). Парсер разбирает по EBNF здесь .

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

ДРУГОЕ ОБНОВЛЕНИЕ

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

 function tokenize(options) {
            var str = options.str;
            var delimiters = options.delimiters.split("");
            var returnDelimiters = options.returnDelimiters || false;
            var returnEmptyTokens = options.returnEmptyTokens || false;
            var tokens = new Array();
            var lastTokenIndex = 0;

            for(var i = 0; i < str.length; i++) {
                if(exists(delimiters, str[i])) {
                    var token = str.substring(lastTokenIndex, i);

                    if(token.length == 0) {
                        if(returnEmptyTokens) {
                            tokens.push(token);
                        }
                    }

                    else {
                        tokens.push(token);
                    }

                    if(returnDelimiters) {
                        tokens.push(str[i]);
                    }

                    lastTokenIndex = i + 1;
                }
            }

            if(lastTokenIndex < str.length) {
                var token = str.substring(lastTokenIndex, str.length);
                token = token.replace(/^\s+/, "").replace(/\s+$/, "");

                if(token.length == 0) {
                    if(returnEmptyTokens) {
                        tokens.push(token);
                    }
                }

                else {
                    tokens.push(token);
                }
            }

            return tokens;
        }

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

3 голосов
/ 14 апреля 2010

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

Для литературы я бы порекомендовал некоторые из старых работ Никлауса Вирта. Он умеет писать. Алгоритмы + структуры данных = Программы - это то, что я использовал, но вы можете найти его Конструктор компиляторов онлайн.

...