Будни компиляторостроителя
Dec. 29th, 2010 03:54 pm( Read more... )
LoadPlugin("leovid.dll")
AviSource("source.avi").ConvertToRGB32().RunLeo("fire.leo")
Его-то я и открываю в VirtualDub'e.
В качестве теста вспомнил детство, когда мы во времена DOS'a и VGA мониторов дергали прерывание 10h для перехода в режим 13h (320x200, 256 цветов) и рисовали там всякие штуки, например, огонь. Помнится, на первом курсе мы соревновались у кого короче получится реализация - рисующая огонь программа занимала около сотни байт. Сейчас модифицировал его алгоритм, чтобы картинка исходного видео задавала вероятности и интенсивности появления источников огня, получился довольно симпатичный эффект:
( Видео )
В оригинале видео более зернистое, ютюб его размазал. Исходник эффекта.
Fun with Leo: metaprogramming for free
Feb. 13th, 2010 12:17 pm# Church numeralsБлагодаря новым оптимизациям, все это компилируется в одну сточку LeoC: print(42). И, соответственно, одну команду ВМ.
zero(f, x) = x
add1(n, f, x) = f(n(f, x))
add(a, b, f, x) = a(f, b(f, x))
mul(a, b, f, x) = a(\t -> b(f, t), x)
one(f, x) = add1(zero, f, x)
two(f, x) = add(one, one, f, x)
three(f, x) = add(one, two, f, x)
four(f, x) = add(two, two, f, x)
six(f, x) = mul(two, three, f, x)
seven(f, x) = add(four, three, f, x)
c42(f, x) = mul(six, seven, f, x)
inc(x) = x + 1
z = c42(inc, 0)
print(z)
Использовать это можно для генерации кода. Например, функция, которая в compile-time создает массив байтов и заполняет его значениями 1..n:
to_int(n) = n(inc, 0)Компилируется в такой код на LeoC:
make(n) = do # на входе - число Черча с количеством требуемых элементов
m = new byte[ to_int(n) ] # создать массив
f(x) = { t = x+1; m[x] <- t; t }
n(f, 0) # f будет вызван и подставлен n раз
m # вернуть массив
end
ten(f,x) = add(four, six, f, x)
a = make(ten)
print(a[|a|-1])
var m_287
m_287 <- new [10]
$Mem[m_287] <-b- 1 $Mem[m_287 + 1] <-b- 2
$Mem[m_287 + 2] <-b- 3
$Mem[m_287 + 3] <-b- 4
$Mem[m_287 + 4] <-b- 5
$Mem[m_287 + 5] <-b- 6
$Mem[m_287 + 6] <-b- 7
$Mem[m_287 + 7] <-b- 8
$Mem[m_287 + 8] <-b- 9
$Mem[m_287 + 9] <-b- 10
var a
a <- m_287
print(byte($Mem[a + 9]))
Неправильный компилятор: оптимизация
Feb. 13th, 2010 11:51 ambound_12 = 19Поэтому вчера добавил еще пару простых оптимизаций: copy propagation и удаление неиспользуемых переменных и вычислений. Первая заключается в том, чтобы пройтись по дереву сгенеренного LeoC-кода и для каждой встреченной переменной запоминать ее "значение":
i_13 = 0
end_13 = bound_12 + 1
while i_13 < end_12 ...
type var_value = Undefined | Copy of rvalue | ComplexUndefined она после объявления и до первого присваивания. Если в присваивании слева стоит переменная, а справа - константа или переменная, то для присваиваемой переменной запоминается, копией чего она является, в противном случае она помечается как Complex. Когда переменная встречается в выражении, смотрится ее "значение" и, если она содержит копию чего-то, то вместо нее подставляется это что-то. После этого выражение упрощается путем constant folding. Кроме этого контекст содержит множество переменных, измененных в текущем блоке кода. Когда встречается if с двумя блоками-ветками, то после него все измененные в ветках переменные помечаются как Complex, т.е. их значения перестают быть известными. Аналогично для цикла, только там сначала делается один проход по телу и собирается набор измененных переменных, а потом во втором проходе уже тело оптимизируется.
Вторая оптимизация идет по дереву LeoC-кода и для каждого блока в первом проходе собирает множество используемых переменных, а во втором проходе выкидывает определения и присваивания переменных, которые нигде не используются.
Вместе они добавили еще 170 строк к компилятору и, во-первых, сделали генерируемый код не таким позорным, а во-вторых, в сочетании с always-inline подходом к компиляции нерекурсивных функций позволили делать прикольные compile-time вычисления, о которых в следующем посте.
Компиляторное
Feb. 10th, 2010 06:39 pmСтруктуры мне нужны для более удобного взаимодействия кода на Leo с кодом программы-хоста. Выглядит это так:
type point = { x : int; y : int } type indata = { len : int str : byte[len] # кстати, это похоже на частный случай зависимых типов ;) ints : int[10] p : point out : byte[len] } args : indata args.p.x <- 50; args.p.y <- 60 for i in args.ints.range args.ints[i] <- i+1 end for i in args.str.range args.out[i] <- args.str[|args.str| - 1 - i] endargs - имя входного параметра программы на Leo (наподобие argc и argv).
В процессе тестирования всплыла очень смешная ошибка: описанный тут метод разбора делал арифметические операции правоассоциативными, в результате чего выражение 3 - 1 - 1 разбиралось как 3 - (1 - 1). Получилось так в ходе борьбы с левой рекурсией в грамматике, правило для сумм и разностей выглядело как
E = P + E | P - E | PЕсли поменять местами P и E, ассоциативность станет нормальной, но возникнет левая рекурсия, что недопустимо. Проблема была решена путем преобразования таких правил в форму
E = P (+ P | - P)*Что в коде выглядит как:
and expr_r s = ( product >>= fun e1 -> p_seq0 ((tok Lplus >>> product >>= fun e2 -> return (Add, e2)) ||| (tok Lminus >>> product >>= fun e2 -> return (Sub, e2))) >>= fun ps -> return (List.fold_left (fun e1 (op, e2) -> Leo.Arith(op, e1, e2)) e1 ps) ) sАналогично для делений с умножениями.
Дальше: оптимизация.
Программы на Leo будут запускаться внутри "настоящих" программ и производить какие-то действия с переданными им данными: проверять валидность регистрационных ключей, проверять контрольные суммы исполняемых файлов, рисовать или не рисовать watermark'и на картинках, расшифровывать строки, расшифровывать байткод других частей, менять структуры программы-хоста (например, сейчас в одном из продуктов код на ВМ динамически меняет тип существующего COM-объекта путем изменения его таблицы виртуальных методов). Т.е. нужно уметь работать с массивами и структурами (по началу хватит и массивов), да нужна целочисленная арифметика. При этом хорошо бы минимизировать количество потенциальных багов, в частности, связанных с выходом за границы массивов и изменяемым состоянием. Плюс, хорошо бы иметь то, что делает функциональные языки (ФЯ) столь удобными, - возможность параметризовать функции другими функциями, в том числе описываемыми прямо в месте передачи, т.е. хорошо бы иметь функции высшего порядка (ФВП) и лямбды.
Итак,
( Read more... )
Часть 3. Первые шаги
Jan. 12th, 2010 06:52 pm
type dst = RegDest of int | PntDest of int;;
type src = Reg of int | Pnt of int | Val of int;;
type 'loc command =
| Arith of oper * dst * src * src
| Mov of dst * src
| Movb of dst * src
| Jmple of 'loc * src * src
| Jmpeq of 'loc * src * src
| Jmp of 'loc
| Print of src
...
Сделал вывод ее в текст в форме, которую можно вставить в текст на Си: int prg[] = { ... };
Тут оказался очень полезен окамловский паттерн-матчинг, особенно когда появились команды, являющиеся частными случаями других:( Read more... )
Часть 2. Иллюзорный автомобиль
Jan. 11th, 2010 12:27 amВиртуальная машина, которая использовалась у меня до этого момента, и в код которой компилился тот DSEL на Руби, была регистровой, причем вообще без стека. Все функции там инлайнились, а рекурсивные функции не поддерживались. Выделением памяти она тоже не занималась - все необходимые буферы должны были быть выделены вызывающей программой и переданы через регистры. Такие ограничения создавали ряд неудобств, поэтому было решено, что в LeoVM нужен какой-то стек и какое-то управление памятью. ( Read more... )
Часть 1. Як, слон и древесная крыса
Jan. 10th, 2010 01:01 pm( Read more... ) Запомните, дети, такие парсер-комбинаторы годятся только для очень простых грамматик.
Но тут во всякую голову придет простая мысль: если проблема в том, что одна и та же работа делается пицот раз, почему бы не делать ее один раз и не запоминать результат. Мысль совершенно очевидная, однако в 2002 году кто-то на ней защитил диплом и дал особое название - ( Read more... )
В итоге весь парсинг языка
Пишем неправильный компилятор. Часть 0
Jan. 9th, 2010 08:57 pmСперва о результате. Пример компилирующегося и работающего кода:
nat = new byte[20] for i in nat.range nat[i] <- i + 1 end # теперь в nat числа от 1 до 20 map(f, xs) = { # составной оператор заключается в {...} или do ... end t = new int[ |xs| ] for i in xs.range, x in xs t[i] <- f(x) end t } iter(f, xs) = for x in xs f(x) end # применить функцию f ко всем элементам xs show(xs) = iter(\x -> print(x), xs) # вывод всех элементов. первый параметр - лямбда even = map(\x -> 2 * x, nat) # even - массив интов с четными числами от 2 до 40 show(even) # выводит 2 4 6 ... 40 show(1..10) # выводит 1 2 3 ... 10 # 1..10 - это не массив и не список, это интервал, заданный двумя числами. sq(x) = x * x squares = map(sq, 1..10) # теперь массив squares содержит 1 4 9 .. 100 sum(xs) = do loop(s, xs) = if |xs| > 0 then loop(xs.head + s, xs.tail) else s loop(0, xs) end print( sum(1..10) ) # выводит 55 print( sum(nat) ) # выводит 210 print( sum(squares ) ) # выводит 385 squares[3..7] <- nat[0..4] # копирует 5 элементов из nat в squares m = new int[9] m <- squares # копирует 9 элементов из squares в m rng = 6..9 m[rng] <- even[rng] show(m) # выводит 1 4 9 1 2 3 14 16 18
( Read more... )
Типизация статическая. Как видите, имеется вывод типов, функции высшего порядка, параметрический полиморфизм, автоматическое управление памятью. Выделение памяти О(1), освобождение О(0). Есть оптимизация хвостовой рекурсии - sum здесь работает в constant space. Язык императивный, но с некоторыми функциональными удобствами. Компилируется в байткод. Вся виртуальная машина и рантайм - сотня строк на Си. Скорость компиляции: данный пример разобрался и скомпилился за 0.05 секунды, типичные целевые программы также компилируются за сотые доли секунды. Скорость работы: втрое быстрее Lua и двое быстрее окамловского байткода (сравнивал на комплексном тесте с нахождением всех простых чисел до 10^7). Причем ВМ на базе while-switch, никакого шитого кода и computed gotos.
( Read more... )