Навіщо реалізовувати лексеру як 2d масив та гігантський комутатор?


24

Я повільно працюю, щоб закінчити свою ступінь, і цей семестр - «Компілятори 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.


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, який "кращий підхід" ви мали на увазі, і яким чином ви б очікували, що він буде "кращим"? Зважаючи на те, що у нас вже є інструменти для лексингу, які добре перевірені, і вони виробляють дуже швидкі аналізатори (як уже говорили інші, DFA, керовані таблицею, дуже швидко), має сенс використовувати їх. Чому ми хотіли б винайти новий спеціальний підхід для конкретної мови, коли ми могли просто написати лексику лексики? Граматика лексу є більш ретельною, і отриманий аналізатор, швидше за все, є правильним (враховуючи, наскільки добре перевірені лекс та подібні інструменти).
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

Пізно до вечірки :-) Маркери підбираються до регулярних виразів. Оскільки їх багато, у вас є двигун multigege, який, в свою чергу, є гігантським DFA.

"Що ще гірше, я не бачу, як це було б віддалено практично, якби мова була здатною до UTF".

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

Отже, у вас є одиночний DFA (який є мультирегексом) для цілого лексеру. Як краще записати його, ніж 2d масив?

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.