Яким має бути тип даних лексем, які лексер повертає до свого аналізатора?


21

Як сказано в заголовку, який тип даних повинен лексеру повернути / надати парсер? Читаючи статтю про лексичний аналіз, яку має Вікіпедія, було зазначено, що:

У інформатиці лексичний аналіз - це процес перетворення послідовності символів (наприклад, у комп’ютерній програмі чи веб-сторінці) у послідовність лексем ( рядків із визначеним «значенням»).

Однак, в повній суперечності з вищезазначеним твердженням, коли відповіли на інше запитання, яке я задав на іншому веб-сайті ( Перегляд коду, якщо вам цікаво), людина, відповідаючи, заявила, що:

Лексер зазвичай зчитує рядок і перетворює це в потік ... лексеми. Лексеми повинні бути лише потоком чисел .

і він зробив це наочне:

nl_output => 256
output    => 257
<string>  => 258

Пізніше у статті, яку він згадав Flex, вже існуючий лексем, і сказати, що писати з ним «правила» було б простіше, ніж писати лексер вручну. Він продовжував наводити мені такий приклад:

Space              [ \r\n\t]
QuotedString       "[^"]*"
%%
nl_output          {return 256;}
output             {return 257;}
{QuotedString}     {return 258;}
{Space}            {/* Ignore */}
.                  {error("Unmatched character");}
%%

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

digit         [0-9]
letter        [a-zA-Z]

%%
"+"                  { return PLUS;       }
"-"                  { return MINUS;      }
"*"                  { return TIMES;      }
"/"                  { return SLASH;      }
"("                  { return LPAREN;     }
")"                  { return RPAREN;     }
";"                  { return SEMICOLON;  }
","                  { return COMMA;      }
"."                  { return PERIOD;     }
":="                 { return BECOMES;    }
"="                  { return EQL;        }
"<>"                 { return NEQ;        }
"<"                  { return LSS;        }
">"                  { return GTR;        }
"<="                 { return LEQ;        }
">="                 { return GEQ;        }
"begin"              { return BEGINSYM;   }
"call"               { return CALLSYM;    }
"const"              { return CONSTSYM;   }
"do"                 { return DOSYM;      }
"end"                { return ENDSYM;     }
"if"                 { return IFSYM;      }
"odd"                { return ODDSYM;     }
"procedure"          { return PROCSYM;    }
"then"               { return THENSYM;    }
"var"                { return VARSYM;     }
"while"              { return WHILESYM;   }

Мені здається, що Flex lexer повертає рядки ключових слів \ лексем. Але це можуть бути повертаючі константи, які дорівнюють певним числам.

Якщо лексер збирається повертати числа, як би він читав рядкові літерали? повернення числа добре для окремих ключових слів, але як би ви мали справу з рядком? Чи не повинен би лексеру перетворити рядок у двійкові числа, і тоді аналізатор перетворив би числа назад у рядок. Лексеру здається набагато логічнішим (і простіше) повернути рядки, а потім дозволити аналізатору перетворити будь-які літеральні рядки чисел у фактичні числа.

Чи міг лексер повернути обидва? Я намагався написати простий лексер на c ++, який дозволяє мати лише один тип повернення для своїх функцій. Таким чином, ведучи мене ставити своє запитання.

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


Лексер повертає те, що ви йому скажете повернути. Якщо ваша конструкція вимагає отримання номерів, вона поверне цифри. Очевидно, що для представлення рядкових літералів знадобиться трохи більше, ніж це. Дивіться також: Це робота Лексера для розбору чисел і рядків? Зауважте, що рядкові літерали взагалі не вважаються "мовними елементами".
Роберт Харві

@RobertHarvey Отже, ви б перетворили рядковий буквал у двійкові числа ?.
Крістіан Дін

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

Отже, що ви говорите, це те, що лексер не читає і не переймається рядковими літералами. І значить, аналізатор повинен шукати ці рядкові літерали? Це дуже заплутано.
Крістіан Дін

Ви можете витратити кілька хвилин на читання цього: en.wikipedia.org/wiki/Lexical_analysis
Роберт Харві

Відповіді:


10

Як правило, якщо ви обробляєте мову через лексику та розбір, у вас є визначення ваших лексичних лексем, наприклад:

NUMBER ::= [0-9]+
ID     ::= [a-Z]+, except for keywords
IF     ::= 'if'
LPAREN ::= '('
RPAREN ::= ')'
COMMA  ::= ','
LBRACE ::= '{'
RBRACE ::= '}'
SEMICOLON ::= ';'
...

і у вас є граматика для аналізатора:

STATEMENT ::= IF LPAREN EXPR RPAREN STATEMENT
            | LBRACE STATEMENT BRACE
            | EXPR SEMICOLON
EXPR      ::= ID
            | NUMBER
            | ID LPAREN EXPRS RPAREN
...

Ваш лексемер приймає вхідний потік і виробляє потік жетонів. Потік лексем використовується аналізатором для створення дерева розбору. У деяких випадках достатньо лише знати тип токена (наприклад, LPAREN, RBRACE, FOR), але в деяких випадках вам знадобиться фактичне значення , пов'язане з маркером. Наприклад, коли ви зустрічаєте маркер ідентифікатора, ви хочете, щоб фактичні символи, які складають ідентифікатор, пізніше, коли ви намагаєтесь зрозуміти, на який ідентифікатор ви намагаєтесь посилатися.

Отже, у вас зазвичай є щось більш-менш подібне:

enum TokenType {
  NUMBER, ID, IF, LPAREN, RPAREN, ...;
}

class Token {
  TokenType type;
  String value;
}

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

if (2 > 0) {
  print("2 > 0");
}
if (0 > 2) {
  print("0 > 2");
}

Вони створюють однакову послідовність типів токенів : IF, LPAREN, NUMBER, GREATER_THAN, NUMBER, RPAREN, LBRACE, ID, LPAREN, STRING, RPAREN, SEMICOLON, RBRACE. Це означає , що вони розібрати те ж саме, теж. Але коли ви насправді щось робите з деревом розбору, ви будете дбати про те, щоб значення першого числа було «2» (або «0») і що значення другого числа було «0» (або «2») ') і що значення рядка дорівнює' 2> 0 '(або' 0> 2 ').


Я розумію більшість того, що ви говорите, але як це String valueзаповнити? це буде заповнено рядком чи цифрою? А також, як би я визначив Stringтип?
Крістіан Дін

1
@ Mr.Python У найпростішому випадку це лише рядок символів, який відповідав лексичній продукції. Отже, якщо ви бачите foo (23, "bar") , ви отримаєте жетони [ID, "foo"], [LPAREN, "("], [NUMBER, "23"], [COMMA, "," ], [STRING "," 23 ""], [RPAREN, ")"] . Збереження цієї інформації може бути важливим. Або ви можете скористатися іншим підходом і матимете значення, яке має тип об'єднання, який може бути рядком або числом тощо, і виберіть потрібний тип значення залежно від типу типу лексеми (наприклад, коли тип лексеми - НОМЕР , використовуйте value.num, а коли це STRING, використовуйте value.str).
Джошуа Тейлор

@MrPython "А також, як би я визначив тип рядка?" Я писав із думки Java-ish. Якщо ви працюєте в C ++, ви можете використовувати тип рядка C ++ або якщо ви працюєте в C, ви можете використовувати знак char *. Справа в тому, що в асоційованому з токеном у вас є відповідне значення або текст, який ви можете інтерпретувати для отримання значення.
Джошуа Тейлор

1
@ ollydbg23 це варіант, а не необгрунтований, але він робить систему менш внутрішньо послідовною. Наприклад, якщо ви хочете, щоб значення рядка останнього міста, яке ви розібрали, тепер вам доведеться чітко перевірити нульове значення, а потім скористатися зворотним пошуком символів на рядок, щоб дізнатися, якою була б рядка. Плюс - це більш жорстке з'єднання між лексером та аналізатором; є більше коду для оновлення, якщо LPAREN може коли-небудь збігатися з різними або кількома рядками.
Джошуа Тейлор

2
@ ollydbg23 Один випадок був би простим псевдо-мініфікатором. Це зробити досить просто parse(inputStream).forEach(token -> print(token.string); print(' '))(тобто просто надрукувати значення рядків жетонів, розділених пробілом). Це досить швидко. І навіть якщо LPAREN може виникнути лише з "(", це може бути постійним рядком в пам'яті, тому включення посилання на нього в маркер може бути не дорожчим, ніж включення нульової посилання. Загалом, я б краще писав код, який не робить для мене особливим випадком код
Джошуа Тейлор,

6

Як сказано в назві, який тип даних повинен лексеру повернути / надати парсер?

"Токен", очевидно. Лексер виробляє потік лексем, тому він повинен повертати потік лексем .

Він згадав про Flex, вже існуючого лексема, і сказав, що писати «правила» з ним було б простіше, ніж писати лексер вручну.

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

Це сказало, кого байдуже, чи це "простіше"? Написання лексеми зазвичай не є важкою частиною!

Коли ви пишете лексер та припускаєте, що він може повернути лише один тип даних (рядки чи числа), який би був більш логічним вибором?

Ні. Зазвичай лексер має операцію "next", яка повертає маркер, тому він повинен повернути маркер . Маркер - це не рядок або число. Це знак.

Останній лексер, який я написав, був лексером "повної вірності", це означає, що він повертає маркер, який відслідковує розташування всього пробілу та коментарі - які ми називаємо "дрібниці" - у програмі, а також маркер. У моєму лексері маркер визначався як:

  • Масив провідних дрібниць
  • Знак жетону
  • Ширина маркера в символах
  • Масив задніх дрібниць

Дрібниці були визначені як:

  • Вигляд дрібниць - пробіл, новий рядок, коментар тощо
  • Ширина дрібниць у символах

Тож якби у нас було щось подібне

    foo + /* comment */
/* another comment */ bar;

що б LEX в вигляді чотирьох маркерів з лексемами видів Identifier, Plus, Identifier, Semicolon, і шириною 3, 1, 3, 1. Першим ідентифікатор має провідну дрібниця , що складається з Whitespaceшириною 4 і задня дрібниця Whitespaceз шириною 1. Plusне мають провідні і дрібниць кінцеві дрібниці, що складаються з одного пробілу, коментаря та нового рядка. Кінцевий ідентифікатор містить провідні коментарі та пробіл тощо.

При такій схемі кожен символ у файлі враховується у висновку лексема, який є зручною властивістю для таких речей, як забарвлення синтаксису.

Звичайно, якщо дрібниці вам не потрібні, ви можете просто зробити два знаки: вид і ширина.

Ви можете помітити, що маркер і дрібниці містять лише їх ширину, а не їх абсолютне положення у вихідному коді. Це навмисно. Така схема має переваги:

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

Якщо вам не важливий жоден із цих сценаріїв, то маркер може бути представлений як вид і зміщення, а не вид і ширина.

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


3

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


Що ви маєте на увазі під м'яко неприємним ? Чи неефективні вони способи отримання рядкових значень?
Крістіан Дін

@ Mr.Python - вони призведуть до безлічі перевірок перед використанням у коді, що неефективно, але ще більше робить код трохи складнішим / крихким.
Теластин

У мене є подібне запитання, коли розробляю лексеми на C ++, я міг би повернути a Token *або просто a Token, або a, TokenPtrщо є спільним покажчиком Tokenкласу. Але я також бачу, що лексери повертають лише TokenType і зберігають значення рядка чи числа в інших глобальних чи статичних змінних. Інше питання - як ми можемо зберігати інформацію про місцеположення, чи потрібно мені мати структуру Token, у якій поля TokenType, String та Location? Спасибі.
ollydbg23

@ ollydbg23 - будь-яка з цих речей може працювати. Я б використовував структуру. А для мов, які не вивчають мови, ви все одно будете використовувати генератор аналізаторів.
Теластин

@Telastyn дякую за відповідь. Ви маєте на увазі, що структура Token може бути чимось схожим struct Token {TokenType id; std::string lexeme; int line; int column;}, правда? Для публічної функції Lexer, наприклад PeekToken(), функція може повернути a Token *або TokenPtr. Я думаю, що це деякий час, якщо функція просто повертає TokenType, як Parser намагається отримати іншу інформацію про Token? Отже, тип даних типу вказівника є кращим для повернення з такої функції. Будь-які коментарі щодо моєї ідеї? Спасибі
ollydbg23
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.