Я повільно працюю, щоб закінчити свою ступінь, і цей семестр - «Компілятори 101». Ми використовуємо «Книгу Драконів» . Незабаром ми переходимо до курсу, і ми говоримо про лексичний аналіз та про те, як його можна реалізувати за допомогою детермінованих кінцевих автоматів (далі - DFA). Налаштуйте різні стани лексерів, визначте переходи між ними тощо.
Але і професор, і книга пропонують реалізувати їх за допомогою перехідних таблиць, які складаються з гігантського 2d масиву (різні нетермінальні стани як один вимір, а можливі вхідні символи як інший) та оператор перемикання для обробки всіх терміналів а також відправлення до таблиць переходу, якщо вони перебувають у нетермінальному стані.
Ця теорія - це все добре і добре, але як хтось, хто насправді пише десятки років, реалізація є мерзенною. Це не перевіряється, це не ремонтопридатне, воно не читабельне, і це біль у півтора налагодження. Що ще гірше, я не бачу, як це було б віддалено практично, якби мова була здатною до UTF. Маючи мільйон записів в таблиці перехідного періоду в нетермінальному стані, поспішає неприємно.
То яка угода? Чому остаточна книга з цього питання говорить, що робити це саме так?
Невже накладні витрати на функціональні дзвінки? Це щось добре працює чи потрібно, коли граматика не відома достроково (регулярні вирази?)? Або, можливо, щось, що займається всіма справами, навіть якщо конкретніші рішення краще працюватимуть для конкретніших граматик?
( зауважте: можливий дублікат " Навіщо використовувати підхід OO замість гігантського оператора переключення? " є близьким, але мене не хвилює OO. Функціональний підхід або навіть більш безпечний імперативний підхід із окремими функціями буде добре.)
Для прикладу розглянемо мову, яка має лише ідентифікатори, і такі ідентифікатори є [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.