Где я должен провести линию между лексером и парсером?

Я пишу lexer для протокола IMAP для образовательных целей, и я в тупике, где я должен рисовать линию между lexer и parser. Возьмите этот пример ответа сервера IMAP:

* FLAGS (\Answered \Deleted)

Этот ответ определяется в формальном синтаксисе следующим образом:

mailbox-data   = "FLAGS" SP flag-list
flag-list      = "(" [flag *(SP flag)] ")"
flag           = "\Answered" / "\Deleted"

Поскольку они указаны как строковые литералы (иначе говоря, "терминальные" токены), было бы более правильным для lexer испускать уникальный токен для каждого, например:

(TknAnsweredFlag)
(TknSpace)
(TknDeletedFlag)

Или было бы правильно исправить что-то вроде этого:

(TknBackSlash)
(TknString "Answered")
(TknSpace)
(TknBackSlash)
(TknString "Deleted")

Моя путаница в том, что прежний метод мог перекомпилировать lexer - если \Answered имел два значения в двух разных контекстах, лексер не выдавал бы правильный токен. Как надуманный пример (эта ситуация не будет возникать, потому что адреса электронной почты заключены в кавычки), как лексер справится с адресом электронной почты, например \[email protected]? Или формальный синтаксис, призванный никогда не допускать возникновения такой двусмысленности?

Ответ 1

Как правило, вы не хотите, чтобы лексический синтаксис распространялся в грамматику, потому что его просто детализировали. Например, лексер для компьютерного программирования langauge, такого как C, безусловно, распознает числа, но обычно нецелесообразно производить токены HEXNUMBER и DECIMALNUMBER, потому что это не важно для грамматики.

Я думаю, что вам нужны самые абстрактные маркеры, которые позволяют вашей грамматике отличать случаи, представляющие интерес, по отношению к вашей цели. Вы получаете возможность опосредовать это путаницей, вызванной одной частью грамматики, выбором, который вы можете сделать в других частях.

Если ваша цель - просто прочитать значения флагов, то на самом деле вам не нужно различать их, а TknFlag без связанного контента будет достаточно хорошим.

Если ваша цель состоит в том, чтобы обрабатывать значения флага отдельно, вам нужно знать, есть ли у вас показания ANSWERED и/или DELETED. То, как они лексически написаны, не имеет значения; поэтому я бы пошел с вашим решением TknAnsweredFlag. Я бы сбросил TknSpace, потому что в любой последовательности флагов должны быть промежуточные пробелы (ваша спецификация говорит так), поэтому я бы попытался устранить, используя любой механизм подавления пробелов, который вы предлагаете lexer.

Иногда я сталкиваюсь с ситуациями, когда есть десятки подобных флагов. Тогда ваша грамматика начинает захламляться, если у вас есть токен для каждого. Если грамматике не нужно знать определенные флаги, тогда у вас должен быть TknFlag со связанным строковым значением. Если для разграничения грамматики требуется небольшое подмножество флагов, но большинство из них нет, тогда вы должны пойти на компромисс: иметь отдельные токены для тех флагов, которые имеют значение для грамматики, и уловить все TknFlag со связанной строкой для остальных.

В отношении сложности двух разных интерпретаций: это один из этих компромиссов. Если у вас есть эта проблема, то ваши жетоны либо должны иметь достаточно мелкие детали в обоих местах, где они необходимы в грамматике, чтобы вы могли различать. Если "\" является релевантным как токен в другом месте грамматики, вы, безусловно, можете создавать как TknBackSlash, так и TknAnswered. Однако, если способ, которым обрабатывается одна часть грамматики, отличается от другой, вы часто можете обойти это, используя лексер, управляемый режимом. Думайте о режимах как о конечной машине состояний, каждая из которых имеет ассоциированный (под) лексер. Переходы между режимами запускаются с помощью токенов, которые являются репликами (у вас должен быть маркер FLAGS, это точно такой сигнал, что вы собираетесь собирать значения флагов). В режиме вы можете создавать токены, которые другие режимы не выдавали бы; таким образом, в одном режиме вы можете создавать токены "\", но в вашем режиме флага вам не нужно. Поддержка режима довольно распространена в лексерах, потому что эта проблема более распространена, что вы можете ожидать. Для примера см. Документацию по Flex.

Тот факт, что вы задаете вопрос, показывает, что вы на правильном пути, чтобы сделать хороший выбор. Вам нужно сбалансировать цель поддерживающейся минимизации токенов (технически вы можете анализировать с использованием токена навсегда ASCII-символа!), С фундаментальным требованием достаточно хорошо различать ваши потребности. После того, как вы построили дюжину грамматик, этот компромисс будет казаться легким, но я думаю, что эмпирические правила, которые я вам предоставил, довольно хороши.

Ответ 2

Я сначала придумал CFG, и какие бы терминалы он не выполнял, это то, что должен знать лексер; в противном случае вы просто догадываетесь о правильном способе токенизации строки.

Ответ 3

Я бы рекомендовал не разделять лексер и парсер - современные подходы к анализу (например, PEG) позволяет смешивать лексирование и синтаксический анализ. Таким образом вам вообще не нужны токены.