Сообщений 6 Оценка 45 Оценить |
Демонстрационная программа - 405 KB
Исходные тексты - 42 KB
REB.sourceforge.net
Согласитесь, в названии этой статьи имеется некоторое противоречие. Общепринятое мнение состоит в том, что интерпретатор не может быть использован в целях низкоуровневого программирования. Однако вспомним - программирование состоит из двух одинаково важных составляющих: правильно организованных данных и продуктивно работающих алгоритмов. Соотношение этих двух частей сильно меняется от задачи к задаче, и именно для упрощения работы со сложными форматами бинарных данных создавался, - а правильней сказать, создается сейчас язык REB.
Автор языка и этой статьи имеет достаточно большой и не всегда приятный опыт быстрого написания программ, обычно на С или С++, которые предназначены по преимуществу для формирования и/или распознавания бинарных форматов. Должен вам сказать, что далеко не всегда в этих случаях нужна высокая скорость работы самой программы, но зато - неизменно требуется очень и очень высокая скорость разработки. А этого семейство языков С/С++, к сожалению, не обеспечивает.
Решение проблемы, достаточно простое и быстрое, пришло мне в голову когда я чисто из спортивного интереса изучал такой замечательный скрипт-язык, как Perl. Там высокая продуктивность работы программиста достигается использованием регулярных выражений (regular expressions). Являясь достаточно сложными для прочтения, регулярные выражения позволяют записать в одной короткой строке достаточно сложный алгоритм, реализация которого на С/С++ потребовала бы куда больших временных затрат. Конечно, в регулярном выражении легко ошибиться. Но зато и легко найти ошибку, именно в силу обозримости кода.
Здесь небходимо сделать краткое отступление в сторону регулярных выражений. С простейшими из них мы сталкиваемся когда занимаемся поиском файлов по расширению: запись "*.cpp" означает, что мы хотим найти все файлы, имена которых состоят из последовательностей произвольных (допустимых) символов, суффиксированных расширением ".cpp". Символ "*" обычно называется квантором, и таких кванторов в языке Perl несколько, хотя они имеют другой смысл.
Символ "." предполагает одно и только одно повторение любого символа. Квантор "*", стоящий после какого-либо символа (или последовательности символов, заключенных в скобки), говорит о произвольном (в том числе и нулевом) количестве повторений символа или последовательности. Например, шаблону (abc.)* соответствуют такие последовательности, как "abcD", "abcfabc9abcn", или даже последовательность "", не содержащая никаких символов вообще.
Еще два квантора: "+" - одно или несколько повторений, "?" - ноль или одно повторение символа или последовательности. Более того, вы можете задать любое нужное вам количество повторений, используя фигурные скобки, например {6} означает ровно 6 повторений, {5,} - от пяти повторений до бесконечности, {3, 80} - от трех до восьмидесяти повторений.
Можно задавать также и семейства символов. Например, [a-zA-Z] означает ни что иное, как произвольный символ латинского алфавита, а [^a-zA-Z] - напротив, произвольный символ, не входящий в латинский алфавит. И, наконец, символ "|" позволяет вам задавать альтернативные шаблоны, например шаблон (c|cpp|h) даст совпадение для любой из трех последовательностей "c", "cpp" и "h".
В таком духе я и начал разрабатывать REB. Сама аббревиатура REB означает "Regular Expressions for Binary Data processing", т.е. регулярные выражения для обработки бинарных данных. Не буду пока углубляться в разъяснение особенностей реализации REB, скажу только, что набор кванторов практически повторяет набор кванторов языка Perl. Основное бросающееся в глаза отличие - то, что основной информативной единицей шаблона REB является не символ, как в Perl, а байт, выраженный двумя шестнадцатиричными символами. Впрочем, и символ и символьная строка могут быть использованы, только символ должен быть заключен в одинарные, а строка - в двойные или обратные кавычки, например как в этом шаблоне: <0a0d "hello, world\n"* 096a6b>. Замечу, что квантор "*" относится в этом примере ко всей строке "hello, world\n". Если нужно проверить повторения символа "\n", то необходимо переписать эту строку так: <0a0d "hello, world" '\n'* 096a6b>. Пробелы, лежащие в "шестнадцатиричной" области, то есть вне кавычек стринга или символа, игнорируются.
В целом разработка шла в духе Perl, однако в некоторый момент оказалось, что логика организации языка REB приводит к еще одному замечательному свойству регулярных выражений, отсутствующему в Perl: к их рекурсивности. В Perl вы можете вставить в ваш шаблон какую-либо переменную, например так:
$hwstring = "hello, world"; $template = "($hwstring|hello)*"; |
Однако присвоить переменной строку, содержащую ссылку на эту-же переменную невозможно, во всяком случае ни к чему хорошему это не приведет, так как переменной $template в вышеприведенном примере присваивается уже готовое строковое значение, где вместо $hwstring подставлена конкретная строка.
Совершенно иная ситуация в REB, где шаблоны являются не строками - пусть даже параметризованными, - а, фактически, функциями. Но здесь пришло время предложить вашему вниманию короткий пример кода. Сразу скажу, что каждая переменная в REB является в то же время функцией, присваивающей значение этой переменной, поэтому запись "x(5);" или даже "x 5;" означает просто-напросто присвоение переменной x значения 5.
Итак, пример. Для демонстрации рекурсивных возможностей REB напишем парсер простейшего функционального языка. Сначала определим шаблон для пробелов, я сделаю это в бинарном виде:
sp <20 | 09 | 0d | 0a>; |
Теперь определимся с тем, что у нас может быть идентификатором:
id <['a'-'z', 'A'-'Z', '0'-'9']+>; |
Напомню, что квантор "+" означает одно или более повторение символа. Теперь зададим хитрый шаблон для аргумента:
argum <$exp ($sp)* | $id ($sp)*>; |
Здесь значок "|", как и в Perl, разделяет альтернативные шаблоны, т.е. обладает своим общепринятым смыслом символа "или". Хитрость же состоит в том, что шаблон exp еще не определен вообще. В REB это вполне допустимо. Определяем шаблон для списка аргументов:
list <$argum ')' | $argum $list>; |
Как видим, приведенный шаблон является рекурсивным, т.е. в нем используется обращение к самому себе. Но пойдем дальше, вот шаблон для функционального выражения - тот, что мы уже использовали в шаблоне arg:
exp <$id '(' $list>; |
Вот и все! Теперь можно задать пример для разбора:
sample "script(do(something) make(something else plus(12 7)))"; |
и проверить, совпадает ли он с шаблоном exp:
result get exp, sample; if less(result, 0), print "Failure.\n"; else print "OK!\n"; |
Здесь функция get вызывается для проверки совпадения шаблона exp с примером sample. Результат возвращается в переменную result, которая будет иметь значение >=0 в случае совпадения. Итак, весь текст парсера:
sp <20 | 09 | 0d | 0a>; id <['a'-'z', 'A'-'Z', '0'-'9']+>; argum <$exp ($sp)* | $id ($sp)*>; list <$argum ')' | $argum $list>; exp <$id '(' $list>; sample "script(do(something) make(something else plus(12 7)))"; result get exp, sample; if less(result, 0), print "Failure.\n"; else print "OK!\n"; |
Конечно, такой парсер пока бесполезен, поскольку мы не можем его использовать для чего-то более сложного, нежели чем проверка факта совпадения с шаблоном. Хотелось бы иметь доступ к тем частям разбираемых данных, которые дали нам совпадение с предоставленным шаблоном. Это вполне возможно, достаточно определить свою функцию, которой будут передаваться результаты разбора:
function printarg, . { arg a; print "argum: $a\n"; }; |
и преобразовать шаблон argum следующим образом:
argum <(~printarg ($exp ($sp)*| $id ($sp)*))>; |
Тильда после открывающей скобки означает то выражение, которому (через стек) будет передан совпавший участок данных в ходе разбора. Поэтому, будучи преобразованным, наш скрипт выдаст на консоль следующую последовательность:
argum: something argum: do(something) argum: something argum: do(something) argum: something argum: something argum: else argum: else argum: 12 argum: 12 argum: 7 argum: plus(12 7) argum: make(something else plus(12 7)) OK! |
Посмотрим на то, как продекларирована функция printarg. Это выглядит несколько странно, но легко объясняется. Дело в том, что в языке REB принципиально отсутствуют ключевые слова, хотя есть определенный набор встроенных функций. Одна из таких функций - "function", извините за невольный каламбур. У нее 2 аргумента: название декларируемой функции и тело функции. Тело определяется с помощью другой встроенной функции, название которой состоит из точки и пробела. Поэтому если обозначить функцию, декларирующую тело, идентификатором body, то все можно переписать так:
function(printarg, body(arg(a), print("argum: $a\n"))); |
Синтаксически такая запись вполне приемлема и понятна интерпретатору, хотя идентификатор body нужно все-таки убрать:
function(printarg, . (arg(a), print("argum: $a\n"))); |
Возвращаясь к шаблонам, должен вам сказать, что нынешний способ использования функций, встраиваемых в шаблон, не вполне меня удовлетворяет. Лучше было бы передавать встроенной функции 3 аргумента: ссылку на разбираемые данные, смещение первого символа из совпавшего участка данных и длину участка. Это будет более экономно и удобно в использовании.
Выше мы рассмотрели случай распознавания данных по регулярному выражению (шаблону). Однако те же шаблоны можно использовать и для формирования данных. К примеру, разработаем наш собственный формат описания растровых образов. Зададим общий формат растрового файла в виде:
bitmap <$header $palette $image>; |
Пусть заголовок файла у нас состоит из названия формата, номера версии в бинарном виде: 0x0001, количества байт в одной строке растра и количества строк:
header <`OurBitmap` 0001 $line_len $lines>; |
Палитра будет состоять из двух частей: количества цветов и самой таблицы цветов:
palette <$num_of_colors $table>; |
Теперь опишем конкретный образ следующим образом:
line_len 10; //количество байт в одной линии растра lines 10; //количество линий num_of_colors set <02>; //количество используемых цветов - 2 table set <00 00000000 11 00ffffff>; //палитра включает лишь два цвета: черный и белый image set < 11 11 11 11 00 00 00 11 00 00 11 00 00 11 00 00 11 11 00 00 11 00 00 11 00 11 00 11 00 00 11 00 00 11 00 00 00 11 00 00 11 00 00 11 00 00 00 11 00 00 11 00 00 11 00 00 00 11 00 00 11 00 00 11 00 00 00 11 00 00 11 00 00 11 00 00 00 11 00 00 11 00 00 11 00 00 00 11 00 00 11 11 11 11 00 11 11 11 11 11 >; |
Теперь для того, чтобы записать готовый бинарный образ файла в переменную bmpfile, достаточно записать:
bmpfile set bitmap; |
Функция set предназначена для формирования бинарных данных из шаблона. Заметим, что тот же шаблон bitmap, после незначительных преобразований, можно будет использовать и для обратной операции - распознавания нашего растрового формата и разбиения его на информативные части.
Далее я опишу некоторые синтаксические особенности REB, а также такие встроенные возможности, как хэши, массивы, ссылки и поддержку объектно-ориентированного программирования.
Итак, синтаксис REB очень прост, он состоит из некоторого списка вызовов функций с аргументами. Список аргументов может быть заключен в круглые, квадратные или фигурные скобки, или же не заключен в скобки вообще, - для интерпретатора это безразлично. Однако, как и в Perl, нужно быть достаточно осторожным в использовании списка аргументов, не заключенного в скобки.
В случае, когда интерпретатор не может найти описанную ранее или встроенную функцию под указанным именем, он заводит новую переменную под этим именем и присваивает ей значение, исходя из контекста. Например, строка
mystr "Hello!"; |
приведет к появлению новой переменной mystr со строковым значением "Hello!". Идентификаторы в REB могут состоять как из обычных символов (т.е. букв латинского алфавита, цифр и символа подчеркивания), так и из специальных символов, предваренных точкой. Например, встроенная функция "less" имеет другой способ записи - ".<". Описанная выше функция описания тела функции имеет только одно специальное имя ". ", состоящее из точки и пробела.
Любой идентификатор может быть предварен символом ":" без какого-либо изменения смысла. Поскольку все переменные REB хранятся в специальном внутреннем хэше, то такие, например, записи идентификатора "less" совершенно эквивалентны:
less :`less` :{`less`} |
Более того, вы можете задать имя переменной, состоящее из достаточно произвольных символов, заключив их в обратные кавычки, например так:
:`@#$\n` 254; |
что приведет к появлению переменной с необычным именем "@#$\n" (содержащим символ перевода строки) и присвоению ей значения 254.
REB предусматривает так-же и параметрические строки, которые должны быть заключены в двойные кавычки. Использование параметрических строк аналогично их использованию в Perl и некоторых других скриптах.
Любая переменная может быть мгновенно превращена в хэш, причем несколькими способами. Самый простой - непосредственно описать все ключи будущего хэша:
x:Aval 12; x:`Bval` 18; x:`C Value Hash`:z 24; x:`C Value Hash`:v 25; |
что приведет к превращению переменной x в хэш с тремя ключами: Aval, Bval и `C Value Hash`, причем последний сам является хэшем с двумя входами - z и v, которым присвоены значения 24 b 25.
Другой способ задать хэш - использовать встроенную функцию "hash", имеющую синонимы ".{" и "object". Тогда предыдущий пример перепишется в виде:
x hash { Aval 12; :`Bval` 18; :`C Value Hash`:z 24; :`C Value Hash`:v 25; }; |
Ключ хэша можно задать и вызовом функции. Например, пусть вызов функции aaa(12) возвращает строку "Aval". Тогда к соответствующему входу хэша x можно обратиться следующим образом: x:{aaa(12)}. В этом случае (в отличие от вызова функции) фигурные скобки обязательны.
Правила работы с массивами практически такие же, за исключением того, что вместо фигурной скобки используется квадратная. Для задания массива можно использовать встроенную функцию array, имеющую синоним ".[":
y array 12, 29, "hi!"; |
что приведет к созданию массива y, проинициализированного значениями 12, 29 и "hi!". Нумерация ведется с нуля, поэтому обращение y:0 возвратит в этом случае 12 (как и y:[0]). Закрывая тему хэшей и массивов, скажу, что любая переменная, являющаяся хэшем, может одновременно являться и массивом, и наоборот.
В настоящее время REB предусматривает два вида ссылок: синонимы, то есть символические ссылки, не подлежащие разыменованию, и просто ссылки, которые можно разыменовать, и на которые тоже можно ссылаться. Пусть, например, описаны следующие переменные:
x:Aval 12; px &x; //ссылка на x y:ppx &px; //ссылка на px внутри хэша y name x, xnm; //xnm - символическая ссылка на переменную x |
Тогда к ключу Aval переменной x можно обратиться следующими способами:
(*px):Aval [px]:Aval [*y:ppx]:Aval (**y:ppx):Aval xnm:Aval |
Ссылаться обоими способами можно на любые переменные, в том числе и на функции.
Как и в Perl, поддержка объектной ориентированности в REB осуществляется с помощью хэша. Но отличия есть, и они делают поддержку ООП в REB более естественной. Каждый хэш в REB имеет предопределенный ключ "self", являющийся символической ссылкой на сам хэш. Кроме того, если функция является членом хэша, то при ее вызове к области глобальной видимости автоматически присоединяется и весь хэш, которому она принадлежит. Приведу пример:
//функция with делает примерно то-же самое, что и hash, но имеет несколько //другую форму записи: with MyClass, do { count 5; //член класса str ` `; //член класса //конструктор класса function construct, . { arg _str; //единственный аргумент конструктора // - новое значение для str copy self, b; //b - новый экземпляр класса MyClass b:str _str; //присваиваем str новое значение return b; }; //а вот и функция - член класса function mprint, . { i count; //обращаемся к count как к члену класса while .>(i, 0), do { print str, "\n"; //обращаемся к str как к члену класса inc i, -1; }; }; }; //конструируем новый объект obj: obj MyClass:construct `OK!`; //Теперь вызываем функцию-член: obj:myprint; |
Теперь посмотрим как осуществляется наследование в REB. Для создания класса-потомка необходимо сделать лишь следующее:
copy MyClass, newMyClass; |
Теперь можно дополнить описание класса newMyClass:
with newMyClass, do { ... }; |
Заметим, что все функции-члены являются виртуальными, как и в Perl. Для использования старого варианта функции предка его необходимо переименовать внутри нового класса:
with newMyClass, do { name mprint, old_mprint; function mprint, . { print "It's newMyClass:print"; old_mprint; }; }; |
В заключение скажу, что работа над REB только начинается, я развиваю его под лизензией GNU и приглашаю к сотрудничеству всех заинтересованных разработчиков. Первый ознакомительный релиз REB (для Cygwin) и исходные тексты доступны интернете здесь и здесь.
Кое что из описанного в данной статье будет изменено и очень многое будет дополнено. Я не описал здесь многих возможных и очень интересных применений REB - например, как универсального ассемблера.
Разработка языка ведется на С++.
Сообщений 6 Оценка 45 Оценить |