Придумуємо лексеми для лексема


14

Я пишу аналізатор для створеної мною мови розмітки (пишу в python, але це не дуже важливо для цього питання - адже, якщо це здається поганою ідеєю, я хотів би запропонувати кращий шлях) .

Я читаю про парсери тут: http://www.ferg.org/parsing/index.html , і я працюю над написанням лексеру, який повинен, якщо я правильно зрозумів, розділити вміст на лексеми. У мене виникають проблеми з розумінням, які типи токенів я повинен використовувати або як їх створити. Наприклад, типи токенів у прикладі, до якого я пов’язаний, є:

  • STRING
  • ІДЕНТИФІЄР
  • НОМЕР
  • БІЛИЙ ПРОСТІР
  • КОМЕНТАР
  • EOF
  • Багато символів, таких як {і (рахуються як власний тип лексеми)

Проблема, яку я маю, полягає в тому, що більш загальні типи токенів здаються мені трохи довільними. Наприклад, чому STRING має свій окремий тип лексеми проти IDENTIFIER. Рядок може бути представлений як STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.

Це також може бути пов'язане з труднощами моєї мови. Наприклад, декларації змінної записуються як {var-name var value}і розгорнуті разом із {var-name}. Схоже , що '{'і '}'повинно бути їх власні маркери, але var_name і VAR_VALUE правомочні лексем типу, або ж вони обидва падають під IDENTIFIER? Більше того, VAR_VALUE насправді може містити пробіл. Пробіл після var-nameвикористовується для позначення початку значення в декларації .. будь-який інший пробіл є частиною значення. Чи цей пробіл стає власною ознакою? Пробіл має в цьому контексті лише таке значення. Більше того, {може не бути початком декларації змінної .. це залежить від контексту (знову це слово!). {:починає декларацію імені та{ можна навіть використовувати як частину якоїсь цінності.

Моя мова схожа на Python в тому, що блоки створюються з відступом. Я читав про те, як Python використовує лексеми для створення лексем INDENT і DEDENT (які більш-менш служать для того, що {і що }можна зробити для багатьох інших мов). Python стверджує, що він не є контекстним, що означає, що принаймні лексему не слід піклуватися про те, де він знаходиться в потоці під час створення маркерів. Як лексер Python знає, що він будує маркер INDENT певної довжини, не знаючи про попередні символи (наприклад, що попередній рядок був новою лінією, тому починайте створювати пробіли для INDENT)? Я запитую, бо мені це теж потрібно знати.

Моє остаточне запитання найглупіше: чому лексеру навіть потрібно? Мені здається, що аналізатор міг би пройти персонаж за персонажем і зрозуміти, де він є і що його очікує. Чи додає лексеру користь від простоти?


2
Перейдіть вперед і спробуйте написати аналізатор без сканера. Якщо він працює взагалі (я думаю, що результат може бути занадто неоднозначним для деяких алгоритмів розбору), швидше за все, ви не побачите жодної фактичної граматики під усіма "пробіл тут також дозволений" і "зачекайте, чи я розбирав ідентифікатор або число? ". Я кажу з досвіду.

Навіщо винаходити колесо на замовлення? Замість того, щоб розробити мову, для якої потрібен спеціальний лексер, ви думали про використання вже існуючої мови, яка вже вбудована лексером, як LISP чи навіть FORTH?
Джон Р. Стром

2
@ JohnR.Strohm для академічних цілей. Сама мова, мабуть, практично не була б корисною.
Вибухові таблетки

Відповіді:


11

Ваше запитання (як підказує останній абзац) насправді не стосується лексеру, а правильного проектування інтерфейсу між лексером та аналізатором. Як ви можете собі уявити, існує багато книг про дизайн лексерів та аналізаторів. Мені здається, мені подобається розбірник Діка Груна , але це може бути не дуже хорошою вступною книгою. Мені трапляється сильно не подобатися Appel-книжці , що базується на С , оскільки код не корисно розширюється у вашому власному компіляторі (через проблеми управління пам'яттю, властиві рішенням зробити вигляд, що C - це як ML). Моїм власним вступом була книга П. Дж. Брауна , але це не гарний загальний вступ (хоча і досить добре для перекладачів). Але повернемось до вашого питання.

Відповідь: зробіть якомога більше в лексемі, не використовуючи обмежень, спрямованих вперед або назад.

Це означає, що (залежно від відомостей мови) ви повинні розпізнати рядок як "символ, за яким послідовність символів не", а потім інший ". Поверніть його в парсер як єдину одиницю. Є кілька причини цього, але важливі

  1. Це зменшує кількість стану, яке парсер повинен підтримувати, обмежуючи його споживання пам'яті.
  2. Це дозволяє лексеру реалізувати увагу на розпізнаванні основних будівельних блоків і звільняє аналізатор, щоб описати, як окремі синтаксичні елементи використовуються для побудови програми.

Дуже часто парсери можуть вжити негайних дій щодо отримання лексеми від лексеру. Наприклад, як тільки отримано IDENTIFIER, аналізатор може здійснити пошук таблиці символів, щоб з'ясувати, чи символ вже відомий. Якщо ваш парсер також розбирає рядкові константи як QUOTE (IDENTIFIER SPASES) * ЦІНА, ви виконаєте безліч нерелевантних пошукових таблиць символів, або в кінцевому підсумку будете піднімати таблицю символів вище, ніж дерево дерева синтаксису парсера, тому що ви можете це зробити це в той момент, коли ви впевнені, що не дивитеся на рядок.

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

Ви можете помітити, що моє опис того, як виглядає рядок, схоже на звичайний вираз. Це не випадково. Лексичні аналізатори часто реалізуються на невеликих мовах (в значенні чудової книги програмування Жемчуків Бентлі Програмування перлів ), які використовують регулярні вирази. Я просто звик думати з точки зору регулярних виразів при розпізнаванні тексту.

Що стосується вашого питання про пробіл, визнайте його в лексемі. Якщо ваша мова має бути досить вільного формату, не повертайте маркери WHITESPACE в аналізатор, тому що їх доведеться лише викинути, тому правила виробництва вашого парсера будуть спамовані з шумом по суті - речі, які слід розпізнати просто для кидання їх геть.

Що стосується того, що це означає про те, як слід поводитися з пробілом, коли це синтаксично важливо, я не впевнений, що я можу зробити для вас судження, яке буде справді добре, не знаючи більше про вашу мову. Моє швидке судження полягає у тому, щоб уникнути випадків, коли пробіл іноді важливий, а іноді - ні, і використовувати якийсь роздільник (наприклад, цитати). Але, якщо ви не можете спроектувати мову будь-яким способом, який ви хочете, ця опція може бути недоступною для вас.

Є й інші способи зробити системи розбору мови розбору. Звичайно, існують системи побудови компіляторів, які дозволяють вказати комбіновану систему лексерів і парсерів (я думаю, що версія Java ANTLR робить це), але я ніколи не використовував її.

Остання історична записка. Десятиліття тому лексеру було важливо зробити якомога більше, перш ніж здати парсер, тому що обидві програми одночасно не вмістилися б у пам’яті. Якщо більше зробити в лексері, залишилося більше пам’яті, щоб зробити аналізатор розумним. Я використовував компілятор Whitesmiths C протягом декількох років, і якщо я правильно зрозумів, він працював би лише в 64 КБ оперативної пам’яті (це була невелика модель MS-DOS) і навіть так переклав варіант C, що був дуже близький до ANSI C.


Хороша історична примітка про розмір пам’яті, що є однією з причин розбиття завдання в першу чергу на лексери та парсери.
stevegt

3

Я візьму на ваше останнє запитання, яке насправді не дурне. Парсери можуть і створюють складні конструкції на основі персонажів. Якщо я пригадую, граматика в Харбісоні та Стіле ("C - Довідковий посібник") має виробництво, яке використовує окремі символи як термінали, і створює ідентифікатори, рядки, числа тощо як нетермінали з окремих символів.

З точки зору формальної мови, все, що лексером на основі регулярних виразів можна визнати та класифікувати як "рядковий буквал", "ідентифікатор", "число", "ключове слово" тощо, навіть розпізнавач LL (1) може розпізнати. Тому немає теоретичної проблеми з використанням генератора парсера, щоб визнати все.

З алгоритмічної точки зору, звичайний розпізнавач виразів може працювати набагато швидше, ніж будь-який аналізатор. З когнітивної точки зору, програмісту, мабуть, простіше розбити роботу між лексером регулярного вираження та записаним парсером-генератором парсера.

Я б сказав, що практичні міркування змушують людей приймати рішення мати окремі лексеми та аналізатори.


Так - і сам стандарт C робить те саме, наче я пам'ятаю правильно, і обидва видання Керніган і Річі.
Джеймс Янгмен

3

Схоже, ви намагаєтесь написати лексеру / парсера, не розуміючи граматики. Зазвичай, коли люди пишуть лексеми та парсери, вони пишуть їх, щоб вони відповідали певній граматиці. Лексеру слід повертати лексеми в граматиці, тоді як аналізатор використовує ці лексеми для відповідності правилам / нетерміналам . Якщо ви зможете легко розібрати свої дані, просто переходячи на байт, то лексери та аналізатори можуть бути надмірними.

Лексери спрощують справи.

Огляд граматики : Граматика - це набір правил щодо того, як повинен виглядати синтаксис або вхід. Наприклад, ось граматика іграшки (simple_command - символ запуску):

simple_command:
 WORD DIGIT AND_SYMBOL
simple_command:
     addition_expression

addition_expression:
    NUM '+' NUM

Ця граматика означає, що
: Simple_command складається з
A) WORD, а потім DIGIT, а потім AND_SYMBOL (це "лексеми", які я визначаю)
B) " add_expression " (це правило або "non-terminal")

Додаток_вираз складається з:
NUM, а потім '+', а потім NUM (NUM - це "маркер", який я визначаю, "+" - буквальний знак плюс).

Тому, оскільки simple_command - це "символ запуску" (місце, з якого я починаю), коли я отримую маркер, я перевіряю, чи вписується він у simple_command. Якщо перший маркер у вхідному слові - WORD, а наступний маркер - DIGIT, а наступний маркер - AND_SYMBOL, то я зіставив деякий simple_command і можу зробити певні дії. В іншому випадку я спробую порівняти його з іншим правилом simple_command, яке є add_expression. Таким чином, якщо першим жетоном був NUM, а потім "+", а потім NUM, то я відповідав простому_команді і вживаю певних дій. Якщо це не те, а у мене синтаксична помилка.

Це дуже-дуже базовий вступ до граматик. Для більш глибокого розуміння ознайомтеся з цією статтею у вікі та пошукайте в Інтернеті підручники з граматики без контексту.

Використовуючи розташування лексера / парсера, ось приклад того, як може виглядати ваш парсер:

bool simple_command(){
   if (peek_next_token() == WORD){
       get_next_token();
       if (get_next_token() == DIGIT){
           if (get_next_token() == AND_SYMBOL){
               return true;
           } 
       }
   }
   else if (addition_expression()){
       return true;
   }

   return false;
}

bool addition_expression(){
    if (get_next_token() == NUM){
        if (get_next_token() == '+'){
             if (get_next_token() == NUM){
                  return true;
             }
        }
    }
    return false;
}

Гаразд, так що цей код некрасивий, і я б ніколи не рекомендував потрійне вкладення, якщо заяви. Але справа в тому, уявіть, що намагаєтеся зробити цю річ над символом замість використання своїх приємних модульних функцій "get_next_token" та "peek_next_token" . Серйозно, дай постріл. Вам не сподобається результат. Тепер майте на увазі, що ця граматика вище приблизно в 30 разів менш складна, ніж майже будь-яка корисна граматика. Чи бачите ви користь використання лексеру?

Чесно кажучи, лексери та парсери - це не основні теми у світі. Я рекомендую спочатку прочитати і зрозуміти граматику, потім трохи прочитати про лексеми / парсери, а потім зануритися.


Чи є якісь рекомендації щодо вивчення граматики?
Вибухові таблетки

Я щойно відредагував свою відповідь, щоб включити дуже базовий вступ до граматик та кілька пропозицій для подальшого вивчення. Граматики є дуже важливою темою інформатики, тому їх варто вивчити.
Кейсі Паттон

1

Моє остаточне запитання найглупіше: чому лексеру навіть потрібно? Мені здається, що аналізатор міг би пройти персонаж за персонажем і зрозуміти, де він є і що його очікує.

Це не дурно, це просто правда.

Але практичність якось трохи залежить від ваших інструментів та завдань. Наприклад, якщо ви використовуєте yacc без лексеру, і хочете дозволити букви Unicode в ідентифікаторах, вам доведеться написати велике і потворне правило, яке в експліциті перераховує всі дійсні символи. Хоча в лексемі ви можете запитати звичайну бібліотечну програму, якщо персонаж є членом категорії букв.

Використовувати лексику або не використовувати її - це рівень абстрагування між вашою мовою та рівнем символів. Зауважимо, що рівень символів на сьогоднішній день - це ще одна абстракція над рівнем байтів, яка є абстракцією вище рівня бітів.

Отже, нарешті, ви могли навіть розібратися на рівні бітів.


0
STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.

Ні, не може. Про що "("? За вашими словами, це не допустимий рядок. І втече?

Взагалі, найкращий спосіб обробляти пробіли - це ігнорувати його, крім межі жетонів. Багато людей віддають перевагу дуже різним пробілам, а виконання правил пробілів є в кращому випадку суперечливим.

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