Я медленно работаю, чтобы закончить свою степень, и этот семестр - Компиляторы 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.