Занял этот процесс полтора рабочих дня (хороших таких дня, когда практически никто не отвлекает). При этом в середине я очередной раз осознал, что интерфейс мне не нравится, и в очередной раз переписал фреймворк, теперь он работает чуть по-другому, чем описано в предыдущем посте. И вот итоги.
Код парсера занимает меньше места: 1032 строки в старом против 620 в новом. При этом есть 14 стандартных постфиксных субпарсера и 21 инфиксный, все они выражаются здесь одной строчкой, в оригинальном парсере - побольше (строчек 5 в среднем, я думаю).
Читать новый парсер и проще и сложнее одновременно. Сложнее потому, что он необычен, непривычен, и потому, что нужно очень хорошо себе представлять стек исполнения во время парсинга многих токенов. В рекурсивном спуске обычно хватает названия метода, парсящего текущий нетерминал, и, может быть, каких-нибудь флажковых параметров. Здесь периодески нужно знать, какие узлы слева уже построились, и при построении каких узлов процесс парсинга вызывался рекурсивно. Самая жестокая моя проверка в этой области звучит так: если наш токен парсится первым в этом стековом фрейме, в следующем слева от нас есть запятая, перед которой мы распарсили декларацию переменной, то идентификатор мы парсим как TypeReference. А иначе (тоже не всегда) - как ReferenceExpression.
Теперь почему читать проще. Потому что поток управления проще. Интереса ради я сравнил количество управляющих ключевых слов, которые смог придумать. Итак:
keyword old new
------------------------------
if >107 57
else if 28 1
else 12 6
while (все) 16 15
while (true) 3 11 (типа простой цикл)
break 6 11
for (java 5) 0 3
return 65 35
Переменные 47 37
Вызовы mark() 28 13
Максимальный уровень блочной вложенности в новом парсере 4, в старом - 7 (к счастью, только в одном месте, в среднем же примерно одинаково).
Вообще, конечно, Пратту такое и не снилось. Он статью писал вообще-то даже не про парсеры, а про языки. Что если придумать язык, который таким вот парсером парсится легко и без проблем, то и человеку он будет совсем понятным. А когда нетерминалы не определяются одним-единственным токеном однозначно, то это есть плохо и непонятно. Отсюда мораль - QL - это не самый понятный для человека язык (согласен, особенно joins).
5 comments:
А вот если бы ты написал бы язык для написания парсеров на MPS, то все было бы намного проще :-)
И кстати, зачем такое невообразимое количество тестов? Ведь код получается практически декларативный. Зачем тестировать все. Это же, по сути все равно, что тестировать геттеры с сеттерами.
МПС бы, сдается мне, не очень сильно помог бы. Ровно по той причине, что код получается практически недекларативный. Декларативным бы он был, если бы язык был читабелен по Пратту. А тут надо ошибки обрабатывать, восстанавливаться, и вообще. Одни joinы распарсить - мерзость полнейшая, декларативно такое я вообще себе не представлю.
И, кстати, декларативный код очень сложно дебажить.
Тестировать надо исключительно затем, чтобы не сломать. Сломать рекурсивный спуск довольно просто, сломать этот новый метод - вообще как нефиг делать.
Я неправильное слово использовал. Не сделать декларативным, а сделать Domain Specific, то есть заDSL-ить.
Так а почему нельзя сделать небольшой язычок для восстанавления. Насколько я понимаю, восстановление обычно представляет из себя такое поведение, когда мы в случае ошибки, пропускаем все лексемы, пока не найдем какую-то лексему, после которой мы можем безопасно продолжить парсировать дальше. Или там все хитрее?
DSL - это правильно, а то особенности синтаксиса анонимных классов в Жаве меня достали. Без них все было бы намного короче.
Но вряд ли в MPS, ибо все-таки хочется, чтобы парсер жил в Идее и был ее частью, а я с трудом себе представляю это с MPS.
С восстановлением да, примерно так оно и есть, только вот в коде оно почему-то каждый раз выглядит чуть-чуть по-разному :). Кстати, а дебажить DSL, думаешь, просто?
Post a Comment