From Novosibirsk, Russia? Our tiny company is looking for current or future rock-star developers.

February 29, 2008

To whom it may concern

Переслушивая в очередной раз для вдохновения Dark Side Of The Moon, решил, что эти слова стоят цитирования.

“…Tired of lying in the sunshine
Staying home to watch the rain
And you are young and life is long
And there is time to kill today
And then one day you find
Ten years have got behind you
No one told you when to run
You missed the starting gun

And you run, and you run to catch up with the sun, but it's sinking
Racing around to come up behind you again
The sun is the same in a relative way, but you're older
Shorter of breath and one day closer to death

Every year is getting shorter
Never seem to find the time
Plans that either come to nought
Or half a page of scribbled lines
Hanging on in quiet desparation is the English way
The time is gone
The song is over
Thought I'd something more to say”

(Pink Floyd “Time”)

February 27, 2008

А вам слабо? ;)

P.S. Осталось сделать то же самое для Ruby, Python и PHP. :)

February 11, 2008

Ищем разработчиков

Немецкая компания KEY-TEC Wiedemann und Konstantin GbR, в которой я проработал год, собирается расширить штат сотрудников в Новосибирске.

Вам предлагается:
— писать на Java под Eclipse и/или OSGi;
— создавать и поддерживать набор клиент-серверных enterprise-приложений, а также сопутствующие веб-сервисы;
— вероятно, будет свой офис, но возможно / в первое время придется работать удаленно, где-нибудь встречаясь;
— гибкий график, можно по желанию работать из дома;
— почасовая оплата в евро, 50–60 тыс. руб в месяц за full time (160 часов).

Требование — «не важно, что вы умеете, лишь бы человек был хороший». Если вы ни разу не использовали ни Java, ни Eclipse, но способны научиться им обоим за неделю, отлично. Прочие требования:
— иногда, возможно, почти каждый день быть в онлайне или приходить на работу — правда, как я уже сказал, офиса может и не быть;
— легко понимать письменный английский и понятно выражаться на нем;
— иметь на работу достаточно времени;
— возлюбить меня как менеджера своего.

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

Если вас интересует такое, пожалуйста, пришлите по адресу andreyvit@gmail.com (т.е. мне) ваше резюме на английском языке.

February 07, 2008

Как работает статический анализ DTL: дизайн движка анализа

(Предполагается, что вы прочти предыдущий пост про алгоритмы.)

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

Анализ начинается с корневой цели, которая передается методу AnalysisEngine.evaluate. При вычислении цели у нее вызывается Goal.evaluate, которому передается объект ContinuationRequestor.

Goal.evaluate (естественно, Goal — это интерфейс, далее я о таком упоминать не буду) может либо вычислить цель (сохранив результаты в ней же) и спокойно завершиться, либо создать объект, реализующий Continuation, и передать его в ContinuationRequestor.subgoal(Continuation).

Continuation, будучи создан Goal'ом, имеет два метода: provideSubgoals и done. Метод provideSubgoal возвращает (вернее, передает переданному ему requestor'у, но это не важно) набор Goal'ов, которые он хочет вычислить. Когда все те Goal'ы вычислены, у предоставившего их Continuation'а вызывается Continuation.done(ContinuationRequestor). Предполагается, что Continuation сохранил goal'ы внутри себя и Continuation.done() может получить из них результаты.

Continuation.done(ContinuationRequestor), аналогично Goal.evaluate(ContinuationRequestor), может либо вычислить результаты goal'а и спокойно завершиться, либо создать новый Continuation.

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

В идеале AnalysisEngine проходится по goal'ам и их continuation'ам в ширину (т.е. сначала проходит по всем имеющимся goal'ам и continuation'ам, вызывая у них evaluate или done; затем проходит по созданным на предыдущем проходе continuation'ам, и т.д.) Однако в нашей реализации производился обход в глубину из-за некоторых утилитарных причин.

Как работает статический анализ DTL: алгоритмы

(Настоящая цель этого поста — дать необходимый контекст для куда более интересного мне поста про дизайн анализа.)

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

Наша IDE использует demand-driven анализ. Его суть проще всего понять на примере, которых ниже достаточно. Выбора между итеративным и demand-driven анализом мы еще когда-нибудь коснемся.

Demand-driven анализ весьма и весьма рекурсивен, поэтому описания алгоритмов тоже будет постоянно ссылаться друг на друга. На самом деле анализ реализован не совсем рекурсией, а промежуточные результаты кэшируются, но мы оставим этот разговор до следующего поста.

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

Кроме того, помимо нод, нам нужно научиться определять:
— тип возвращаемого значения конкретного метода конкретного класса;
— тип локальной переменной;
— тип аргумента;
— тип поля;
— тип глобальной переменной.

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

Идентификатор. Сначала мы проверяем, есть ли класс с таким именем. Если есть, то тип идентификатора — этот самый класс. Если нет, то ищем в текущей области видимости переменную с таким именем. Мы найдем либо локальную переменную, либо аргумент, либо поле, либо глобальную переменную. Запускаем анализ для них, и ответ выдаем в качестве результата.

Вызов метода. Запускаем анализ для выражения слева от точки. Для всех классов, которыми может быть то выражение, ищем метод с таким именем. Получаем набор кандидатов. Далее запускаем анализ для каждого аргумента. Когда он выполнен, запускаем анализ возвращаемого значения каждого из кандидатов, передавая ему тип получателя (т.е. выражения слева от точки) и типы аргументов.

Локальная переменная. Проходим AST текущего метода и находим все присваивания этой переменной. (Т.е. находим текстуально все присваивания переменной с таким именем.) Запускаем анализ для правой части каждого присваивания. Результаты объединяем.

Аргумент. Делаем то же, что и для локальной переменной. Затем, если в текущий метод мы пришли из другого метода, то типы аргументов в конкретной точке вызова были уже вычислены, и мы просто используем их, объединяя с результатами анализа для локальной переменной. Если же типы аргументов не известны, то мы находим все вызовы нашего метода, и в каждом вызове запускаем анализ для соответствующего аргумента. Результаты объединяем.

Дополнительный финт: если описанный выше подход не вычислил ни одного возможного класса для нашего аргумента, то мы собираем все имена методов, которые вызываются у данного аргумента, и отбираем классы, имеющие все эти методы (за исключением методов, которых нет ни в одном классе — их, вероятно, написали с ошибкой или еще не реализовали). Это позволяет делать completion в методах, которые только что созданы или которые никто не вызывает.

Поле. Тривиально. Мы проходим по всем методам класса, в каждом находим все присваивания переменной с таким именем (да, для прототипа мы не заботились о сокрытии имен — можно было еще проверить, что это действительно ссылка на поле), для каждого запускаем анализ типа правой части. Результаты объединяем.

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

Тип возвращаемого значения метода. Находим в методе все операторы return, вызываем анализ для выражения в каждом из них, результаты объединяем.

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

Массивы. Когда мы встречаем присваивание переменной вида a[2] = x, мы запоминаем, что присваивание выполнялось переменной a, но вычисленный тип x нужно сначала обернуть в тип-массив.

Когда нам нужно вычислить тип a[2], мы запускаем анализ для a, затем среди возможных типов переменной отбираем типы-массивы и разворачиваем их, получая типы элементов.

Индекс. Поскольку обход 35 тыс. строк программы занимает около 50 мс и требуется для каждой глобальной переменной (которых мало, что дает около секунды) и каждого метода (которых много, что дает десятки секунд), мы предварительно обходим всю программу, запоминая все вызовы методов и все присваивания переменным, и группируя их по именам (так, что за константное время по заданному имени можно найти все его вызовы и все присваивания ему).

Статический анализ. Статический анализ, например, существования методов запускает анализ типа каждого выражения слева от точки, и смотрит, есть ли среди методов возможных типов этого выражения вызванный метод. Если нет, то фиксируется ошибка. Остальные виды сделанных нами анализов столь же тривиальны.

February 06, 2008

DTL IDE

За прошедшую неделю (в компании с fourdman'ом) создан прототип IDE для очень малоизвестного (in-house) языка DTL (Dynamically Typed Language). В комплекте:
— частично восстанавливающийся парсер на ANTLR;
— вычисление типов с обработкой массивов;
— отображение иерархии типов;
— отображение иерархии вызовов (на базе вычисления типов);
— статические проверки существования методов, количества аргументов и наличия определений переменных (язык требует явных определений).

Отсутствует: инкрементальный анализ, учет локального control flow функций, честный data flow для значений, соответственно, и честная обработка коллекций (но наш частичный вариант неплохо работает на реальном коде), распараллеливание анализа.

На всё вычисление типов ушло примерно 3 дня работы одного человека. Парсер потребовал необычайно много времени (около 4 дней, наверное), статические анализы приделаны примерно за пару часов. За иерархии типов в DLTK надо кое-кого поиметь, но об этом fourdman может поведать куда больше и красочнее.

После вечера и ночи за (в основном ручным) профилированием полный статический анализ 36 тыс. строк кода на DTL (с вычислением всех типов) занимает 2 секунды на Core 2 Duo 2.2 GHz (тогда как еще вчера днем это было около 60 секунд).

Из DLTK использована поддержка UI для Quick Type Hierarchy и для Call Hierarhy, Outline и Script Explorer. Чтобы это всё легко заработало, пришлось юзать ASTNode (но все конретные ноды наши) и строить языковую модель DLTK.