Зачем реализовывать лексер как 2d массив и гигантский коммутатор?


24

Я медленно работаю, чтобы закончить свою степень, и этот семестр - Компиляторы 101. Мы используем Книгу Дракона . Вскоре в курсе, и мы поговорим о лексическом анализе и о том, как он может быть реализован с помощью детерминированных конечных автоматов (далее DFA). Настройте различные состояния лексера, определите переходы между ними и т. Д.

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

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

Так в чем же дело? Почему окончательная книга на эту тему говорит, что это так?

Действительно ли накладные расходы на вызовы функций настолько велики? Это то, что хорошо работает или необходимо, когда грамматика не известна заранее (регулярные выражения?)? Или, может быть, что-то, что обрабатывает все случаи, даже если более конкретные решения будут работать лучше для более конкретных грамматик?

( примечание: возможный дубликат « Зачем использовать ОО-подход вместо гигантского оператора switch? » близок, но я не забочусь о ОО. Функциональный подход или даже более разумный подход с автономными функциями подойдет.)

И в качестве примера рассмотрим язык, который имеет только идентификаторы, и эти идентификаторы есть [a-zA-Z]+. В реализации DFA вы получите что-то вроде:

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(хотя что-то, что правильно обработало бы конец файла)

По сравнению с тем, что я ожидал:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

С кодом в NextTokenрефакторинге в свою собственную функцию, если у вас есть несколько пунктов назначения с начала DFA.


5
наследие древних (1977) Принципы Compiler Design ? 40 лет назад стиль кодирования сильно отличался
комнат

7
Как бы вы осуществили переходы государств DFA? А что касается терминалов и нетерминалов, «нетерминалы» обычно относятся к правилам производства в грамматике, которые следуют после лексического анализа.

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

5
Если какая-то часть вашего раздражения исходит от знания того, как лучше выполнять свою работу, и от отсутствия возможности получить какую-либо обратную связь или признательность за подход, который вы бы предпочли - так как десятилетия в индустрии учат нас ожидать обратной связи, а иногда и оценки - возможно, Вы должны написать свою лучшую реализацию и опубликовать ее в CodeReview.SE, чтобы получить часть этого для вашего собственного спокойствия.
Джимми Хоффа

7
Простой ответ заключается в том, что лексер обычно реализуется как конечный автомат и генерируется автоматически из грамматики, а таблица состояний, что неудивительно, наиболее легко и компактно представляется в виде таблицы. Как и в случае с объектным кодом, тот факт, что людям нелегко работать, не имеет значения, потому что люди не работают с ним; они меняют источник и генерируют новый экземпляр.
Кешлам

Ответы:


16

На практике эти таблицы генерируются из регулярных выражений, которые определяют токены языка:

number := [digit][digit|underscore]+
reserved_word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

У нас были утилиты для генерации лексических анализаторов с 1975 года, когда был написан lex .

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


4
Я не уверен, что предлагаю это оптом. Регулярные выражения будут иметь дело с произвольными (регулярными) языками. Нет ли лучших подходов при работе с конкретными языками? Книга затрагивает прогностические подходы, но затем игнорирует их в примерах. К тому же, выполнив наивный анализатор для C # несколько лет назад, мне было не очень сложно поддерживать его. Неэффективное? конечно, но не так ужасно, учитывая мои навыки в то время.
Теластин

1
@Telastyn: почти невозможно идти быстрее, чем управляемый таблицей DFA: получить следующий символ, найти следующее состояние в таблице переходов, изменить состояние. Если новое состояние терминальное, выведите токен. В C # или Java любой подход, предусматривающий создание любых временных строк, будет медленнее.
Кевин Клайн

@kevincline - конечно, но в моем примере нет временных строк. Даже в C это будет просто индекс или указатель, проходящий через строку.
Теластин

6
@JimmyHoffa: да, производительность определенно актуальна для компиляторов. Компиляторы работают быстро, потому что они были оптимизированы до чертиков и обратно. Не микрооптимизации, они просто не выполняют ненужную работу, такую ​​как создание и удаление ненужных временных объектов. По моему опыту, большая часть коммерческого кода для обработки текста выполняет одну десятую часть работы современного компилятора и занимает в десять раз больше времени. Производительность огромна, когда вы обрабатываете гигабайт текста.
Кевин Клайн

1
@Telastyn, какой «лучший подход» вы имели в виду, и каким образом вы ожидаете, что он будет «лучше»? Учитывая, что у нас уже есть хорошо протестированные инструменты lexing, и они производят очень быстрые парсеры (как говорили другие, основанные на таблицах DFA очень быстрые), имеет смысл использовать их. Почему мы хотели бы придумать новый особый подход для конкретного языка, когда мы могли бы просто написать лексическую грамматику? Грамматика lex более удобна в обслуживании, и результирующий синтаксический анализатор, скорее всего, будет правильным (учитывая, насколько хорошо протестированы lex и подобные инструменты).
DW

7

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

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


7

Внутренний цикл:

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

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

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


5

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

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


2

Проработав ранее «Книгу Дракона», основная причина наличия рычагов и парсеров, управляемых таблицами, заключается в том, что вы можете использовать регулярные выражения для генерации лексера и BNF для генерации парсера. В книге также рассказывается, как работают такие инструменты, как lex и yacc, и по порядку, чтобы вы знали, как работают эти инструменты. Кроме того, для вас важно проработать несколько практических примеров.

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

Надеемся, что ваш инструктор также разрешит вам использовать lex и yacc (если только это не выпускной класс и вы не напишете lex и yacc).


0

Поздно к вечеринке :-) Жетоны сопоставляются с регулярными выражениями. Поскольку их много, у вас есть движок multi-regex, который, в свою очередь, является гигантским DFA.

«Что еще хуже, я не понимаю, как это было бы отдаленно практично, если бы язык был способен UTF».

Это не имеет значения (или прозрачно). Кроме того, UTF обладает хорошим свойством, его сущности не перекрываются даже частично. Например, байт, представляющий символ «A» (из таблицы ASCII-7), больше не используется ни для какого другого символа UTF.

Итак, у вас есть один DFA (который является регулярным выражением) для всего лексера. Как лучше записать это, чем 2d массив?

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.