Нормальні парсери, як їх зазвичай навчають, мають лексерну стадію, перш ніж аналізатор торкнеться введення. Лексер (також "сканер" або "токенізатор") розбиває вхід на невеликі лексеми, які позначаються типом. Це дозволяє основному аналізатору використовувати лексеми як термінальні елементи, а не потребувати трактування кожного символу як терміналу, що призводить до помітного підвищення ефективності. Зокрема, лексер також може видалити всі коментарі та пробіл. Однак окрема фаза токенізатора означає, що ключові слова також не можуть бути використані як ідентифікатори (якщо мова не підтримує перехід, який дещо не прихильний, або префіксує всі ідентифікатори подібними до сигілів $foo).
Чому? Припустимо, у нас є простий токенізатор, який розуміє такі лексеми:
FOR = 'for'
LPAREN = '('
RPAREN = ')'
IN = 'in'
IDENT = /\w+/
COLON = ':'
SEMICOLON = ';'
Токенізатор завжди буде відповідати найдовшому маркеру і віддасть перевагу ключовим словам над ідентифікаторами. Так interestingбуде lexed , як IDENT:interesting, але inНЕ буде lexed , як IN, буває так IDENT:interesting. Фрагмент коду, як
for(var in expression)
буде переведено в потік токенів
FOR LPAREN IDENT:var IN IDENT:expression RPAREN
Поки що це працює. Але будь-яка змінна inбуде використана як ключове слово, INа не змінна, яка б порушила код. Лексер не зберігає жодного стану між маркерами, і не може знати, що inзазвичай має бути змінною, за винятком випадків, коли ми знаходимося в циклі for. Також наступний код повинен бути легальним:
for(in in expression)
Перший inбув би ідентифікатором, другий - ключовим словом.
Є дві реакції на цю проблему:
Контекстуальні ключові слова заплутані, давайте повторно використовувати ключові слова.
У Java є багато зарезервованих слів, деякі з яких не мають жодного користі, крім надання корисніших повідомлень про помилки програмістам, які переходять на Java з C ++. Додавання нових ключових слів код перерви. Додавання контекстних ключових слів заплутає читача коду, якщо вони не мають гарного підкреслення синтаксису, і ускладнює втілення інструментів, тому що їм доведеться використовувати більш досконалі методики розбору (див. Нижче).
Коли ми хочемо розширити мову, єдиним розумним підходом є використання символів, які раніше не були легальними для мови. Зокрема, це не можуть бути ідентифікатори. За допомогою синтаксису циклу foreach Java повторно використала існуюче :ключове слово з новим значенням. За допомогою лямбда, Java додала ->ключове слово, яке раніше не могло виникнути в жодній юридичній програмі (як -->і раніше воно буде лексиковане, як '--' '>'законне, і ->раніше воно могло б бути використане як ліцензія '-', '>', але цю послідовність буде відхилено парсером).
Контекстуальні ключові слова спрощують мови, давайте їх реалізувати
Лексери безперечно корисні. Але замість запуску лексеру перед парсером, ми можемо запустити їх у тандемі з парсером. Розбіжники знизу вгору завжди знають набір типів токенів, який був би прийнятний у будь-якому місці. Потім аналізатор може попросити лексеру відповідати будь-якому з цих типів у поточній позиції. У циклі для кожного циклу аналізатор буде знаходитись у положенні, позначеному ·в (спрощеній) граматиці після того, як змінна знайдена:
for_loop = for_loop_cstyle | for_each_loop
for_loop_cstyle = 'for' '(' declaration · ';' expression ';' expression ')'
for_each_loop = 'for' '(' declaration · 'in' expression ')'
У цій позиції, юридичні лексеми SEMICOLONабо IN, але не IDENT. Ключове слово inбуло б абсолютно однозначним.
У цьому конкретному прикладі аналізатори зверху вниз також не матимуть проблем, оскільки ми можемо переписати вищезазначені граматики на
for_loop = 'for' '(' declaration · for_loop_rest ')'
for_loop_rest = · ';' expression ';' expression
for_loop_rest = · 'in' expression
і всі маркери, необхідні для прийняття рішення, можна побачити без зворотного відстеження.
Розглянемо зручність використання
Java завжди прагнула до смислової та синтаксичної простоти. Наприклад, мова не підтримує перевантаження оператора, оскільки це зробить код набагато складнішим. Отже, приймаючи рішення між синтаксисом циклу inта :для кожного циклу, ми повинні враховувати, що менш заплутане та більш очевидне для користувачів. Можливо, крайній випадок
for (in in in in())
for (in in : in())
(Примітка. У Java є окремі простори імен для типів імен, змінних та методів. Я думаю, що це помилка, здебільшого. Це не означає, що пізніший дизайн мови повинен додати більше помилок.)
Яка альтернатива забезпечує чіткіше візуальне розділення між змінною ітерації та ітераційною колекцією? Яку альтернативу можна розпізнати швидше при погляді на код? Я виявив, що розділення символів краще, ніж рядки слів, якщо мова йде про ці критерії. Інші мови мають різні значення. Наприклад, Python пише англійською мовою багато операторів, щоб їх можна було прочитати природним шляхом і їх легко зрозуміти, але ті самі властивості можуть ускладнити розуміння фрагмента Python з першого погляду.