Monday, February 4, 2008

Афихеть

В Идее есть лимитированная поддержка AOP. Там есть небольшой язычок для описания мест в коде, его парсер писал я примерно с год назад. По ходу написания случайно изобрел парсер-комбинаторы, потом понял, что код получается не очень читабельным, и совсем не дебаживаемым. После основного написания периодически выяснялось, что язычок умеет чуть больше, чем я, приходилось этот парсер подхачивать, в итоге он приобрел совсем жуткий вид.

И вот когда, наконец, в релизе еще один юзер заметил еще одну неподдерживаемую фичу в языке, я таки решил посмотреть, как же такую неоднозначную грамматику парсят сами создатели. Скачал исходники, нашел там парсер их Джавы, и... Что я вижу? Классы PrefixParser и InfixParser, метод ExprParser#parse с параметром-приоритетом, ну и вааще. Итого, они используют Пратта! При этом в коде он нигде не упоминается, но это типичный он. Ну, чуть измененный, притом не так, как у меня... Возможно, они его сами изобрели. Возможно, нет. Не знаю. Но сам факт!

Ложка дегтя. Так они парсят только Жаву. Сам аоп парсится обычным рекурсивным спуском.

Saturday, January 5, 2008

Развлекся

Есть у нас в Идее язык по имени QL, эдакая упрощенная версия SQL, диалекты которой используются в EJB и Hibernate. На вид несложный, однако в парсере периодически приходится туговато. Парсер, естественно, написан рекурсивным спуском, двумя с половиной разными людьми, и выглядит достаточно страшно. Я, в качестве добровольного каникулярного развлечения, попытался написать то же самое с использованием Праттовского метода. Тожесамовость измеряется duck-критерием: есть 141 тест, и на них нужно выдавать такое же дерево, что и старый парсер. В код старого парсера я смотрел нечасто, поэтому то, что незатестировано, соответственно, не воспроизведено.

Занял этот процесс полтора рабочих дня (хороших таких дня, когда практически никто не отвлекает). При этом в середине я очередной раз осознал, что интерфейс мне не нравится, и в очередной раз переписал фреймворк, теперь он работает чуть по-другому, чем описано в предыдущем посте. И вот итоги.

Код парсера занимает меньше места: 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).

Saturday, December 29, 2007

Как писать парсеры

Обычно парсеры языков программирования пишутся в виде грамматики, которая либо скармливается потом генератору, либо уже представляет собой код с использованием парсер-комбинаторов. К сожалению, если язык сложен, грамматика его тоже непроста. А сложным, надо сказать, становится любой язык, когда в него нужно добавить разумную обработку ошибок и восстановление после таковых, и при этом выдать какое-то достаточно вменяемое синтаксическое дерево на выходе. Ровно это необходимо для хорошей поддержки языка в IDE.

Поэтому в самой знакомой мне IDE парсеры пишутся обычно руками, рекурсивным спуском, скучно, занудно и однообразно (чего стоит один практически идентичный код, раскопированный для каждой бинарной операции). Восстановление после ошибок тогда получается достаточно несложно.

Так вот, не рекурсивным спуском единым жив человек. Уже почти устоялось API для другого метода, придуманного когда-то Праттом, примененного Крокфордом, до боли напоминающего вдохновленный языком Forth метод Тузова, наконец, дошедшего до меня и творчески переработанного. Про это API и расскажу.

Итак, по старинной фортовской привычке мы начинаем с объявления данных кодом. Данные у нас - это поток токенов от лексера, вот они-то и будут кодом. У каждого токена будет свой собственный маленький кодик, вот такого вида:

interface TokenParser {
boolean parseToken(PrattBuilder builder);
}

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

С каждым токеном ассоциируется еще и число, приоритет. Самый главный метод праттбилдера носит скромное название parse, принимает одно число, и идет последовательно, каждый раз для текущего лексерного токена вызывая его токенпарсеры, пока приоритеты токенов строго больше числа-параметра либо пока какой-нибудь токенпарсер не вернет false. Сами токенпарсеры внутри себя могут делать что угодно, сдвигать лексер насколько угодно (в том числе путем вызова билдеровского parse рекурсивно), но не в их власти откатиться раньше токена, для которого их вызвали. Билдер всегда помнит место, в котором вызвали его parse, и умеет создавать узлы дерева, начинающиеся там.

Это была фортовская часть. Теперь к Пратту. Самый частый вариант токенпарсера, DefaultTokenParser, реализован заветами этого товарища, и умеет различать два случая вызова себя и делегировать всю работу одному из двух подпарсеров - соответствующих своих аргументов конструктора. Первый случай, "свежачок", когда токен встретился сразу же в билдеровском parse, слева от него ничего еще нет, у Пратта это зовется nud, у меня пока - PrefixParser (better name needed!!!). Второй случай, когда слева уже что-то есть, и там даже построился узел дерева с определенным типом, у Пратта - led, у меня пока - ProceedingParser (oh name...). Первое используется, когда по токену можно понять, какую конструкцию он начинает (унарная префиксная операция), второй - какую конструкцию он продолжает (инфиксная бинарная операция или много чего еще) или закачивает (унарная постфиксная операция).

У токена нечасто определены оба компонента, самый очевидный пример токена, умеющего и начинать и продолжать - "+" или "-". Если nud или led нужен, но отсутствует, генерируется ошибка. Токен, который мы анализировали, нам уже неинтересен, мы с него все, что хотели, получили, поэтому перед вызовом подпарсеров лексер сдвигается вперед. Оба подпарсера возвращают тип узла, который надо построить от стартового маркера до текущего места. Обычно алгоритм разбора токенпарсера по умолчанию получаются следующим: вызываем nud первого токена, а потом, пока не посинеем, вызываем led у текущих токенов.

Пример простого нуда на псевдоязыке:

unary(IElementType type, int priority) = fun (builder) ->
builder.parse(priority);
return type

И еще проще:

tokenElement(IElementType type) = fun (builder) ->
return type

Пример простого леда (бинарная левоассоциативная операция) почти дословно такой же:

binary(IElementType type, int priority) = fun (leftType, builder) ->
builder.parse(priority);
return type

Постфиксная операция еще проще:

postfix(IElementType type) = fun (leftType, builder) ->
return type

Распарсить простые выражения можно теперь с помощью весьма лаконичного кода:

NUMBER: priority=10, nud=tokenElement(NUMBER_LITERAL)
IDENTIFIER: priority=10, nud=tokenElement(QNAME)

"(": priority=10, nud=fun (builder: PrattBuilder) ->
builder.parse(0);
builder.assertToken(")");
return PARENTHESES

".": priority=9, led=binary(QNAME, 9)
"!": priority = 8, nud=unary(NEGATION, 8)

"+","-": priority=8,
nud=unary(ARITHMETIC_UNARY, 8),
led=binary(ARITHMETIC_BINARY, 6)
"*","/": priority=7, led=binary(ARITHMETIC_BINARY, 7)

Парсер, например, для сокращенного определения класса в Java лучше выражается через просто токенпарсеры:

"public", "static", etc.: priority = -100,
tokenParser = fun (builder) ->
builder.advance();
return true;
"class": priority=-100,
tokenParser = fun (builder) ->
builder.finishPrefix(MODIFIER_LIST);
builder.advance();
builder.assertToken(IDENTIFIER);
if (builder.assertToken(EXTENDS)) {
builder.advance();
builder.parse(9);
}
builder.assertToken("{");
builder.parse(-90);
builder.assertToken("}");
return JAVA_CLASS

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

В данный момент на праттовской основе (только, увы, в синтаксисе Java) в достаточной степени написан парсер для языка шаблонов FreeMarker, и процесс мне нравится. Парсер получается более прямолинейный, проверок условий в нем намного меньше, чем в обычном рекурсивном спуске. Это можно рассматривать как паттерн Visitor, он же полиморфизм, мы вместо делания многочисленных проверок на тип вызываем абстрактный метод у самого типа.

Из достоинств еще можно упомянуть модулярность. Можно теоретически взять и поменять парсер или приоритет у токена. Что важнее, можно легко начать парсить не сначала, а из любой середины, почти с любого токена, что открывает неплохие возможности для инкрементального репарса.

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

Еще особенность, уж не знаю, достоинство или недостаток. При написании парсера надо думать. Этот парсер не совсем похож на грамматику, и не пишется на автомате, глядя на таковую. Скорее всего, такие парсеры тоже можно генерировать по грамматике, для этого надо только погрузиться в соответствующую науку. Но это уже совсем другая история.

Saturday, December 22, 2007

Запруды

Ну вот как можно заниматься наукой в такой антисемитской обстановке? Вводная: нужно написать программу, которая бы делала из одних строк на произвольном языке другие по определенным правилам. Произвольный язык подразумевает под собой юникод, в частном случае хотя бы кириллицу.

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

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

Видимо, на камле надо ставить крест в данной конкретной задаче. Да, под него есть юникодовая библиотека, я ее даже поставил себе, даже попытался на нее все переделать, но теперь программа уже знает, что ей даются строки в  UTF-8. И это не очень гармонирует с замыслом. Да и просто все это элементарно неудобно. Варианты камля, компилящиеся в .NET некузявы по причине того, что у меня мак. Вот.

Можно все писать на Scheme. Забавный язычок такой, интересно было бы с ним поэкспериментировать. Правда, IDE для него, хоть и сильно удобнее камлевого топлевела, все же смущает MDIшностью своего интерфейса. Еще я так до сих пор не до конца привык к динамической типизации. И, что самое главное, под имеющуюся у меня Схему нет большого количества библиотек общего назначения. В жизни вообще нужно не очень много утилит, но вот SortedSet и SortedMap были бы неплохи. Да и просто Set тоже... В чистом виде я их под Схему не нашел.

Библиотеки в избытке есть в JDK, но на Жаве я писать в принципе не хочу. Так что самый пока приемлемый вариант - это Scala. Надо будет поразбираться с эклипсовским ее плагином, пока идейский не дорос... Пока что не очень понял, как там тесты легко и просто писать, найденный мной пример легким и простым не назовешь никак. Ну и вообще, многословный язык, по сравнению с камлем, хаскелем или лиспом, хоть и сильно лучше жавы. Плохо все.

Monday, December 3, 2007

peter-mac:~ peter$

В субботу на НЛП-семинаре рассказывали нам про то, как работает система машинного перевода ПроМТ. Собственно, ничего особо неожиданного не случилось: для каждого направления перевода есть много-много заданных вручную правил, как переводить в данном направлении конкретные слова и словосочетания. Правила для последних чаще всего имеют вид шаблонов, то бишь, содержат переменные. В общем, ничего особо концептуального. Работать отлично это, видимо, не способно по определению, но можно добиться более-менее сносных результатов, что мы и имеем.

Процесс организован следующим образом. Сидит лингвист, смотрит на какой-нибудь неправильный промтовый перевод, размышляет. Придумывает новое правило, которое все разом возьмет и полечит. Идет к программисту, объясняет правило, программист это дело кодит. Далее есть 18М тестовых текстов (или текстовых тестов?), на которых прогоняются старый и новый варианты, и просматриваются отличия (hopefully их немного). Если отличия случились в плохую сторону, лингвист идет думать дальше, иначе изменения принимаются.

Если я все правильно понял, весело быть программистом в ПроМТе. Сидишь, никого не трогаешь, приходят к тебе красивые девушки и рассказывают про правила какого-нибудь экзотического языка. Кругозор, понимаешь, расширяют. Кайф!

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

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

Thursday, November 22, 2007

Табличко



Мобильнотелефонные камеры - это зло, но самое главное видно.

Sunday, November 4, 2007

Минута на размышление

Нурали Латыпов, "Основы интеллектуального тренинга. Минута на размышление". Книга про то, как мы думаем, и как этот процесс можно улучшить. Предполагается, что прочитав эту книгу, человек, если не улучшит его, то, по крайней мере, поймет, как это надо делать. Основной метод очень прост, я с ним полностью согласен, и относится он вообще ко всему в этой жизни: чтобы научиться что-то делать, надо это делать. Чаще думайте, друзья мои, и будет вам счастье.

Только вот кроме этого все как-то достаточно туманно. Ну да, бывает ТРИЗ, синектика и ландаматика. Надо их всех освоить и применять. Искать, что же это такое, предлагается самому. Ну по ТРИЗу у меня некоторое количество книг понакачано. По слову "ландаматика" гугль выдает 5 результатов на русском языке, ничего концептуального. Синектики и то больше. Правда, вряд ли я все это изучу. Лениво как-то, с ТРИЗом пытался уже. Так и буду дальше мыслить по старинке, нетворчески...

Много наездов на российское образование, дескать, учит оно запоминать кучу сведений, а вот творчески их использовать не учит. И надо учить детей сразу творчеству. Дальше много текста, который вроде как призван описать, что же именно надо делать, только по прочтении в голове не остается вообще ничего. Интересно, камль способствует развитию творческого мышления?

Достаточное количество веселых баек из истории науки про всяких Эйнштейнов и прочих Резерфордов, как у них все было творчески. Некоторые байки взяты даже не из "Физики шутят", и я их даже раньше не знал. Спасибо автору.

Также спасибо за развеяние ложного ощущения, что я достаточно знаю про всякие экзотические искусственные языки. Оказалось, не все. Бывает язык Диал, по идее отражающие наиболее универсальные универсалии Универсума. Название происходит от слова "диалектика". Такое вот чудо создали в 80х в МИФИ, и с тех пор о нем никто не слышал. Ну, почти никто. Гугль вот крайне немного про него слышал. Зато в том же МИФИ есть УРА - Университет Русского Альтруизма. Виртуальный. В интернете выложены разные лекции по разным экзотическим курсам, вроде Теории Универсалий или этого самого диала. Попробовал почитать то и другое, как-то не пошло. Мозг успешно сопротивляется философским текстам. А жаль. Ведь если выучить диал, то, по мнению создателей, будешь изрекать исключительно творческие вещи.