Maria Arkhipova, Sophia Zelenova
© 2007 Институт Системного Программирования
РАН
UniTesK Lab
Представление синтаксиса целевого языка
Неформальное описание семантики калькулятора
1. Спецификация правила: “Имена объявляемых переменных уникальны”
2. Спецификация правила: “Переменная должна быть объявлена до использования”
3. Спецификация правил, описывающих ограничения на типы
Текстовое представление TreeDL-узлов
Приложение A. Пример файла srl.properties для STG-проекта
Приложение B. Пример файла stg.properties для STG-проекта
Приложение D. Пример файла project.xml для STG-проекта
Приложение E. Пример файла project.properties для STG-проекта
Инструмент SemaTesK предназначен для автоматической генерации семантических тестов на целевом языке. Для создания пакета тестов требуется
Прочитав данный документ, вы сможете написать спецификации и сгенерировать тесты для языка простого калькулятора.
Обычно для синтаксиса целевого языка имеется описание грамматики в виде какой-либо разновидности BNF.
Язык нашего калькулятора имеет следующий синтаксис:
program ::= ( stmt )+; stmt ::= var_decl | assign | output; var_decl ::= type <ID> "=" expr ";" ; assign ::= <ID> "=" expr ";" ; output ::= "print" expr ";" ; type ::= "int" | "double"; expr ::= arg ( "+" arg )* ; arg ::= var | const; var ::= <ID>; const ::= <INT> | <DOUBLE> ; |
Здесь | разделяет альтернативы правила, ? означает, что последовательность внутри скобок может отсутствовать, + указывает на список из 1 или более элементов, а * - на список из 0 или более элементов. <ID>, <INT> и <DOUBLE> являются лексемами, т.е. обозначают элементы, принадлежащие некоторым множествам (строк-идентификаторов, целых чисел и действительных чисел двойной точности соответственно). Мы будем строить TreeDL-представление для синтаксиса, опираясь на это описание.
Полное описание языка TreeDL можно найти на сайте проекта TreeDL в разделе Documentation.
TreeDL-описание включает в себя заголовок и описание TreeDL-узлов. Вот общий вид заголовка, для TreeDL-файла, подаваемого на вход SemaTesK:
[ translate.language = "java"; visitor.name = "<имя интерфейса визитера>"; startnode.name = "<имя узла стартового правила>"; target.language = "<общий префикс имен>"; target.extension = "<расширение файлов, содержащих тесты>"; all_tests_file_name[1] = "<имя файла, в который будут записаны текстовые представления всех сгенерированных тестов>"; ] tree ru.ispras.redverst.stg.langdep.modl.<общий префикс имен>Tree : <com.unitesk.atp.tree.TreeClass>; header{...} |
В начале в квадратных скобках перечислены
свойства, необходимые инструменту SemaTesK. Затем указано полное имя описания
дерева
ru.ispras.redverst.stg.langdep.modl.<общий префикс>Tree |
и базовое описание
<com.unitesk.atp.tree.TreeClass> |
Блок header{} должен содержать импорты всех классов, используемых в данном TreeDL-описании.
Заметим, что имя базового TreeDL-описания указано в угловых скобках. Вообще всякий внешний для данного TreeDL-описания java-тип должен указываться в угловых скобках, кроме тех случаев, когда он присутствует во вставках java-кода.
В нашем случае в заголовок имеет вид:
[ translate.language = "java"; visitor.name = "CalcVisitor"; startnode.name = "Program"; target.language = "Calc"; target.extension = "clc"; ] tree ru.ispras.redverst.stg.langdep.modl.CalcTree : <com.unitesk.atp.tree.TreeClass>; header{} |
После заголовка перечисляются описания узлов. Всякий узел в TreeDL-описании, подаваемом на вход инструменту SemaTesK, должен быть наследником класса
ru.ispras.redverst.stg.base.BaseNode |
Этот класс импортируется автоматически, поэтому можно использовать его короткое имя.
Теперь мы опишем правила составления TreeDL-описания по BNF-описанию.
program ::= ( stmt )+; |
ему мы сопоставим такой узел TreeDL-описания:
node Program : <BaseNode> { child Stmt+ stmts; } |
Здесь, как и в BNF знак + означает список из одного или более элементов.
stmt ::= var_decl | assign | output; |
мы опишем
четыре узла:
abstract node Stmt : <BaseNode> {} node VarDecl : Stmt { ... } node Assign : Stmt { ... } node Output : Stmt { ... } |
type ::= "int" | "double"; |
будет
задаваться такими узлами:
abstract node Type : <BaseNode> {} node IntType : Type {} node DoubleType : Type {} |
node Id : <BaseNode> { attribute <String> name; } |
Правило, описывающее определение переменной,
var_decl ::= type <ID> "=" expr ";" ; |
будет задано
узлом
node VarDecl : Stmt { child Type type; child Id id; child Expr expr; } |
ВАЖНО! Для текущей версии SemaTesK имя поля (ребенка или атрибута) в TreeDL-описании считается идентификатором этого поля. Поэтому во всем TreeDL-описании не должно быть полей с одинаковыми именами. В случае несоблюдении этого условия при генерации могут возникнуть ошибки из-за того, что инструмент принял одно поле за другое.
Задание[2]: напишите узлы для остальных правил грамматики.
Вот такое TreeDL-описание должно получиться в итоге:
[ translate.language = "java"; visitor.name = "CalcVisitor"; startnode.name = "Program"; target.language = "Calc"; target.extension = "clc"; ] tree ru.ispras.redverst.stg.langdep.modl.CalcTree : <com.unitesk.atp.tree.TreeClass>; header{}
node Program : <BaseNode> { child Stmt+ stmts; }
abstract node Stmt : <BaseNode> {}
node VarDecl : Stmt { child Type type; child Id id; child Expr expr; }
node Assign : Stmt { child Id assId; child Expr assExpr; }
node Output : Stmt { child Expr expr; }
abstract node Type : <BaseNode> {}
node IntType : Type {}
node DoubleType : Type {}
node Expr : <BaseNode> { child Arg+ args; }
abstract node Arg : <BaseNode> {}
abstract node Const : Arg {}
node IntConst : Const { attribute <int> intValue; }
node DoubleConst : Const { attribute <double> doubleValue; }
node Var : Arg { child Id varId; }
node Id : <BaseNode> { attribute <String> name; } |
ВАЖНО! Желательно, чтобы в TreeDL-файле наследники одного узла указывались бы в порядке от простого к сложному (в смысле семантики узлов), т.к. указанный в TreeDL-файле порядок используется инструментом при итерации. В случае несоблюдения этого правила при генерации могут возникать нежелательные ситуации. Именно поэтому в нашей записи сначала идут константы IntConst и DoubleConst, а затем переменная Var, т.к. переменная имеет более сложную семантику, чем константы (например, для нее требуется существование декларации).
Семантика нашего калькулятора включает следующие правила:
Описание семантики состоит из списка описаний семантических связей.
Рассмотрим
первое семантическое правило для калькулятора Имена объявляемых переменных
уникальны. Для него мы в SRL-файле определим семантическую связь:
many-to-many relation UniqVarName { comment "1_UniqVarName" ... } |
Здесь перед служебным словом relation
указывается тип семантического правила. В
нашем случае одному источнику может соответствовать много целей, и одной цели
много источников (т.к. наша связь устанавливается между любыми двумя
декларациями переменных). Поэтому тип связи будет many
_
to
_
many
.
Далее после служебным словом relation
задается идентификатор правила.
ВАЖНО! Идентификаторы всех правил должны быть уникальны в рамках одного SRL-документа. Идентификатор правила представляет собой последовательность латинских букв и цифр и может начинаться с любой латинской буквы в верхнем или нижнем регистре, с символа '_' или '@'.
После служебного слова comment
можно
задать текстовое описание данного правила. Это не обязательно, но настоятельно
рекомендуется.
Первым делом определим типы узлов, между которыми может быть установлена наша семантическая связь. Связываемые узлы мы будем называть узлом-целью и узлом-источником.
Данное правило определяет семантическую связь между любыми двумя декларациями переменных, поэтому типы цели и источника будут совпадать с типом узла VarDecl, определяющего инструкцию декларации переменной:
target VarDecl {…} source VarDecl {…} |
В фигурных скобках должны быть указаны корни поддеревьев деревьев с вершинами в вершине-источнике и в вершине-цели соответственно. Таких поддеревьев, что их вид зависит от данного семантического правила. В данном случае в правиле говорится о том, что имена всех переменных должны быть уникальны, следовательно, в фигурных скобках и для источника и для цели будет указано имя дочернего узла VarDecl, а именно id, соответствующего имени переменной:
target VarDecl { id } source VarDecl { id } |
ВАЖНО! При
написании семантических связей надо иметь в виду, что семантическая связь
определяется именем узла-цели, списком в фигурных скобках
для
узла-цели, именем узла-источника и списком в фигурных скобках для
узла-источника. Нельзя написать две связи с одинаковыми указанными элементами,
надо всю семантику для таких связей описывать в одном месте. Иначе при работе
инструмента могут возникнуть ошибки. Кроме того в фигурных скобках не должно
быть ссылки на attribute,
только на child.
Порядок, в
котором будут идти декларации, для данной семантической связи нам не важен. Это
мы укажем с помощью модификатора arbitrary, сразу после конструкции comment
:
comment "1_UniqVarName" arbitrary |
Далее, мы укажем
окружение, в котором должно применяться данное правило и могут встречаться
источник и цель. Поскольку в нашем случае связь может быть установлена между
любыми двумя декларациями, то в качестве корня их общего поддерева мы укажем
главный узел Program
:
context c1: same Program |
Для одного правила можно указать
несколько контекстов, поэтому для того, чтобы их различать контексты именуются.
В данном случае имя контекста c1
.
ВАЖНО! В пределах одного правила имена все контекстов должны быть различны.
Модификатор same
указывает на то, что поддерево
с корнем Program
является общим для источника и цели.
ВАЖНО! Контекст описывает минимальное относительно источника и цели поддерево, имеющее корень указанного типа.
Итак, описанная нами семантическая связь имеет следующий вид:
many_to_many relation
UniqVarName { comment
"UniqVarName" arbitrary unequal target
VarDecl { id } source
VarDecl { id } context
c1: same
Program } |
Следующее правило, для которого мы будем описывать семантические связи, звучит так: Переменная должна быть объявлена до использования.
Здесь у нас ситуация сложнее, чем с первым правилом, т.к. существуют разные способы использования переменной. Наше правило разбивается на три более простых правила:
Для каждого из этих подправил мы построим свою семантическую связь.
Рассмотрим первое подправило. Оно описывает семантическую связь между инструкцией присваивания и декларацией переменной, встречающейся в этой инструкции. Вот эту связь мы и опишем.
Сначала определим тип семантической связи. Поскольку переменная из левой части инструкции присваивания должна иметь ровно одну декларацию, а одна декларация может относиться к разным таким переменным, то связь будет иметь тип one_to_many:
one_to_many relation ExistenceOfVarDeclaration_2_1 { comment "Existence of var declaration for left part of assignment" … } |
Как и раньше, нам нужно задать тип узла-источника и тип узла-цели. В данном случае целью будет "целевой" узел Assign, описывающий инструкцию присваивания, а источником – узел VarDecl, описывающий декларацию переменной. В фигурных скобках после описания узла-цели следует указать имя дочернего узла цели соответствующего имени переменной в левой части присваивания, а именно: assId.
target Assign { assId } source VarDecl {…} |
После описания узла-источника в фигурных скобках следует указать имя
дочернего узла узла VarDecl,
а именно: id. Потому что именно этот
дочерний узел соответствует имени переменной.
target Assign { assId } source VarDecl { id } |
Для данной связи важен порядок вывода, а, именно, узел-цель должен выводиться после узла-источника. Поэтому вместо модификатора arbitrary, указывающего на произвольный порядок вывода, мы напишем модификатор semantic:
semantic |
Так как нет никаких ограничений на то, где должно выполняться правило и должны встречаться инструкции присваивания и соответствующая ей декларация, то в качестве окружения мы снова укажем стартовый узел Program:
context c1: same Program |
Модификатор same указывает на то, что поддерево с
корнем Program
является общим для источника и цели. Как и в предыдущем случае, имя контекста - c1
.
Вот полное описание получившейся семантической связи:
one_to_many relation ExistenceOfVarDeclaration_2_1 {
comment "Existence
of var declaration for left part of assignment" semantic equal target
Assign { assId } source
VarDecl { id } context
c1: same
Program } |
Задание: напишите семантическую связь для второго подправила: Для переменной, используемой в выражении (такую переменную описывает узел Var), должна существовать декларация переменной с таким же именем. (Указание: рассуждайте так же, как и в случае первого подправила).
Следующее правило, для которого мы будем описывать семантические связи, звучит так: Идентификатор переменной v, используемой в правой части декларации d, должен быть отличным от идентификатора переменной, определяемой декларацией d.
Это правило описывает семантическую связь между идентификаторами в пределах одной инструкции, декларирующей переменную.
Сначала определим тип семантической связи. Поскольку имя переменной из левой части инструкции присваивания не должно совпадать с именем никакой переменной из правой части этого присваивания, то связь будет иметь тип many_to_many:
many_to_many relation SelfInitInVarDeclaration { comment "In var declaration var must not be initialized by itself" … } |
Определимся сначала с контекстом правила. В данном случае в качестве контекста удобно указать декларацию переменной:
context c1: same VarDecl |
Как и раньше, нам нужно задать тип узла-источника и тип узла-цели. В
данном случае целью укажем декларацию переменной, которая также является корнем
контекста. Для того чтобы это сделать надо воспользоваться специальным приемом,
который позволяет ссылаться на объекты в пределах одного правила:
target this.target_context {…} |
Служебное слово this указывает на то, что далее следует ссылка на объект данного правила, а target_context позволяет сослаться на контекст цели. Когда контексты цели и источника совпадают, принято ссылаться на контекст цели[3].
В качестве источника укажем узел типа Var, соответствующий переменной справа от знака присваивания:
source Var {…} |
После описания источника и цели в фигурных скобках следует указать имена
дочерних узлов источника и цели соответственно, обозначающих имена переменных:
target this.target_context { id } source Var { varId } |
Для данной связи неважен порядок вывода:
arbitrary |
Вот полное описание получившейся семантической связи:
many_to_many relation SelfInitInVarDeclaration {
comment "In
var declaration var must not be initialized by itself" arbitrary unequal target
this.target_context {id
} source
Var { varId } context
c1: same
VarDecl } |
Рассмотрим следующие правила из описания семантики нашего калькулятора:
1.
Если один из аргументов выражения имеет
тип double, то все выражение имеет тип double, если же все аргументы выражения
имеют тип int, то выражение имеет тип int.
2.
Тип int является подтипом типа double.
Т.е. любой элемент, имеющий тип int, автоматически является элементом типа
double.
3.
В декларации тип выражения-правой части
должен быть подтипом типа описываемой переменной.
4.
В инструкции присваивания тип правой
части должен быть подтипом типа левой части.
Первое правило описывает способ вычисления некоторой вспомогательной величины называемой "тип". Второе правило описывает соотношения между возможными значениями этой величины (таких значений два – int и double). Третье и четвертое правила описывают ограничения на значения величины "тип" для некоторых элементов.
Замечание: вообще говоря, первое правило необходимо дополнить еще правилами вычисления типов для констант и переменных, например, такими: Если в декларации переменной стоит модификатор int, то она имеет тип int. Если в декларации переменной стоит модификатор double, то она имеет тип double. Если в записи константы точка отсутствует, то константа имеет тип int. Если в записи константы точка присутствует, то константа имеет тип double. Часто в неформальном описании семантики подобные подробности могут опускаться, как сами собой разумеющиеся. Мы же составляем формальное описание семантики и поэтому не можем пренебрегать такими "мелочами".
Для того чтобы автоматизировать с помощью STG описание правил статической семантики типизируемых языков, в SRL были введены понятия семантический тип, семейство типов, семантически типизированная вершина, совместимость и приведение типов.
Вершины в TreeDL-описании, моделирующие типы целевого языка, должны наследовать базовый тип вершин ru.ispras.redverst.stg.semrel.TypeDesc из библиотеки stg_lib-draft-2.0.jar. Следовательно, изменим TreeDL-спецификацию нашего языка
abstract node Type : <TypeDesc> { } node IntType : Type { } node DoubleType : Type { } |
Кроме того, для вершин, моделирующих конструкции целевого языка, для которых определено понятие типа, должна быть определена дочерняя вершина с именем type и типом, унаследованным от TypeDesc. В нашем примере такими вершинами являются Expr и Arg. Описание этих узлов в TreeDL-спецификации примет вид:
node Expr : <BaseNode> { child Arg+ args; child Type type; } abstract node Arg : <BaseNode> { child Type type; attribute <Integer> value;
} |
Для того чтобы не писать дополнительные правила, задающие типы констант, определим только один узел, описывающий константы, и будем выводить значение константы в зависимости от ее типа:
node Const : Arg { body { public Object newValue() { if
(this.type instanceof IntType) { return
new Integer((value.intValue()>=Byte.MAX_VALUE
|| value.intValue()<=Byte.MIN_VALUE ? value.intValue() : Byte.MAX_VALUE - 10)); } if
(this.type instanceof DoubleType) { return
new Double("1.00000001" +
value.intValue()); } throw new IllegalArgumentException("type"); } public
String toString() { return
newValue().toString(); } } } |
Замечание: Обратите внимание, что при внесении последних изменений в TreeDL-описание узел Const перестал быть абстрактным, а узлы IntConst и DoubleConst вообще больше не нужны.
Метод Const#toString() надо использовать при разработке конвертера тестов из внутреннего представления в текстовое.
Все типы данных, существующие в целевом языке, подразделяются
на семейства типов. В одном семействе типов должны находиться совместимые (в терминах семантики
целевого языка) типы данных. В нашем языке есть два типа Integer и Double, которые должны быть отнесены к одному семейству типов,
так как величины типа Integer автоматически приводятся к типу Double. Описание данного семейства типов будет выглядеть
следующим образом:
type_set PrimitiveTypes { ref IntType, ref
DoubleType } |
Теперь мы можем перейти непосредственно к формализации семантических правил, перечисленных в начале раздела.
Рассмотрим первое правило:
Если один из аргументов выражения имеет тип double, то все выражение
имеет тип double, если же все аргументы выражения имеют тип int, то выражение
имеет тип int.
Данное правило означает, что если один из аргументов выражения имеет тип double, тогда и все выражение имеет тип double, а в том случае, когда такого аргумента нет, тип выражения может считаться как int, так и double, поскольку тип int приводим к типу double.
Таким образом, контекст данного правила может быть описан так:
context c1: same Expr |
В качестве цели рассмотрим корень контекста, а качестве источника
аргумент выражения:
target this.target_context
{…} source Arg {…} |
Если тип аргумента double, тогда тип
выражения тоже должен быть double. Для описания
таких требований надо использовать специальную конструкцию языка SRL – фильтр. Фильтр
указывается в виде требования в квадратных скобках после описания вершины, на
которую накладывается требование. В данном случае фильтр будем накладывать на
описание атрибутов источника и цели:
target this.target_context {
type[is DoubleType]
}
source Arg { type[is DoubleType] } |
Поскольку данное семантическое правило фактически является требованием на существования дочерней вершины с именем type типа DoubleType в поддереве, начинающемся в вершине-цели, при условии существования такой вершины в поддереве, начинающемся в вершине-источнике, то в данном правиле вместо ключевых слов equal или unequal, задающих способ вычисления атрибутов, используем ключевое слово present.
Тип семантического правила будет many-to-many, так как требуется рассмотреть все пары Expr-Arg.
В итоге мы получим семантическое правило следующего вида:
many_to_many relation
ExprType_Double { comment
"Expression type is defined by the type of
its arguments: if expr is of type Double it means that there is at least one
double argument in this expression" arbitrary present target
this.target_context
{ type[is
DoubleType] } source
Arg {
type[is DoubleType]
}
context c1:
same Expr } |
Поскольку семантическое требования о совместимости типов int и double мы уже описали посредством задания семейства типов, то перейдем сразу к следующему правилу.
Сначала определим контекст семантического правила. Укажем в качестве контекста декларацию переменной:
context c1: same VarDecl |
Теперь в качестве цели укажем узел, соответствующий инициализирующему выражению в правой части:
target Expr { type } |
так как мы
собираемся потребовать, чтобы тип Expr был приводим к типу объявленной
переменной. Операция приведения типов некоммутативная, а язык SRL позволяет
требовать приводимости типов в поддереве цели к типам в поддереве источника.
Для этого используем ключевое слово compatible.
compatible target Expr {
type } source this.target_context
{ type } context c1: same VarDecl |
Compatible означает, что данное правило накладывает следующие ограничения на типы аргументов источника и цели:
1. типы аргументов источника и цели должны быть унаследованного от TypeDesc;
2. типы аргументы источника и цели должны принадлежать одному семейству типов;
3. тип цели должен располагаться в описании семейства типов слева от типа аргумента источника, либо оба типа должны принадлежать одной группе типов.
Данное
семантическое правило может иметь тип one-to-many или one-to-one. В данном случае все равно, какой
тип выбрать, так как в указанном контексте может быть только одна пара
источник-цель. Для того чтобы явно подчеркнуть это свойство данного правило
выберем тип one-to-one.
В итоге семантическое правило будет иметь вид:
one_to_one relation
AssignTypeCompatibility { comment
"The type of assignment right part must be
compatible to the type of the assignment left part" arbitrary compatible target
Expr { type } source
this.target_context { type } context c1:
same VarDecl } |
Перейдем к описанию последнего правила: В инструкции присваивания тип правой части должен быть подтипом типа левой части.
В качестве контекста рассмотрим инструкцию ¾ присваивание:
context c1: same Assign |
В принципе данное правило мало, чем отличается от предыдущего с той лишь разницей, что тип переменной в левой части присваивания вычисляется в другом правиле, а именно в ExistenceOfVarDeclaration_2_1. Для того чтобы сослаться на вершину дерева, участвующую в другом правиле ExistenceOfVarDeclaration_2_1, воспользуемся следующей конструкцией:
this.target_context.ExistenceOfVarDeclaration_2_1[target=prev].source |
Это описание пути по синтаксическому дереву с учетом установленных между вершинами дерева семантических связей с целью нахождения типа переменной, указанной в левой части присваивания. Здесь Assign обозначает вершину, соответствующую инструкции-присваиванию, ExistenceOfVarDeclaration_2_1 указывает на семантическую связь, установленную между вершинами рассматриваемого дерева. Далее в квадратных скобках следует фильтр, который указывает на то, что требуется выбрать семантическую связь, целью в которой является вершина, обозначающая рассматриваемую инструкцию-присваивание. После того как такая семантическая связь найдена, нужно взять ее источник, который будет соответствовать декларации переменной, в которой задается тип.
Поскольку связи типа ExistenceOfVarDeclaration_2_1 должны быть установлены в дереве прежде, чем будут устанавливаться связи описываемого типа, требуется явно указать зависимость между соответствующими семантическими правилами:
… relation AssignTypeCompatibility_2
: ExistenceOfVarDeclaration_2_1 { comment
"The type of assignment right part must be
compatible to the type of the assignment left part" … source this.target_context.ExistenceOfVarDeclaration_2_1[target=prev].source {type} … } |
Задание: допишите семантическое правило AssignTypeCompatibility_2. (Указание: рассуждайте так же, как и в предыдущем случае).
Далее следует описать правило вычисления типа переменной в зависимости от ее декларации. Делается это абсолютно аналогично предыдущему правилу.
Задание: попробуйте самостоятельно написать семантическое правило AssignTypeCompatibility_3. (Указание: рассуждайте так же, как и в предыдущем случае).
Язык sscl предназначен для написания семантических ограничений на синтаксис. Т.е. таких ограничений, которые в принципе могли бы быть описаны средствами грамматики, но не были там описаны по каким-то причинам (например, если для описания ограничения нужно вводить в грамматику много дополнительных правил).
Поскольку в нашем примере синтаксические ограничения полностью описаны в грамматике, то файл calc.sscl будет пустым.
Для того чтобы сгенерированные тесты выводились, нужно написать отображение из TreeDL-представления в текстовое.
За вывод текста отвечает компонент-принтер с именем
ru.ispras.redverst.stg.langdep.visitors.<общий префикс>VisitorPrinter |
В нашем случае общий префикс равен Calc (мы задавали его в TreeDL-файле), и полное имя принтера такое:
ru.ispras.redverst.stg.langdep.visitors.CalcVisitorPrinter |
Класс CalcVisitorPrinter должен реализовать интерфейс CalcVisitor и наследоваться от библиотечного класса ru.ispras.redverst.stg.base.visitors.VisitorPrinter. Таким образом, следует включить в описание класса следующий код
import ru.ispras.redverst.stg.base.visitors.VisitorPrinter; import ru.ispras.redverst.stg.langdep.modl.CalcTree.*; import ru.ispras.redverst.stg.semrel.SemanticRelation; import ru.ispras.redverst.stg.base.BaseNode; import ru.ispras.redverst.stg.exceptions.StgException; |
О классе VisitorPrinter можно подробнее узнать на сайте сайте проекта TreeDL.
Декларация класса CalcVisitorPrinter должна выглядеть так:
public class CalcVisitorPrinter extends VisitorPrinter implements CalcVisitor { … } |
Метод java.util.List start( BaseNode node ) интерфейса CalcVisitor нам не нужен, поэтому мы просто вернем из него пустой список:
public java.util.List start( BaseNode node ) throws StgException { return new java.util.ArrayList(); } |
Для всякого неабстрактного узла мы должны вывести его текстовое представление. Делается это в соответствующем visit-методе. Строковое представление передается во внутренний буфер с помощью специальных функций (txt(String), nl(), list(...) и др.).
Напишем вывод текстового представления идентификатора. Нам нужно просто напечатать его значение. Для этого мы вызовем функцию txt(...), передав ей форматированную строку[4]:
public void visitId( Id node ) throws StgException { txt( "${name}" ); } |
Здесь выражение ${name} означает, что при формировании текста сюда будет вставлено текстовое значение атрибута name текущего узла node. Если в скобках после $ написано имя ребенка, а не атрибута, то будет вызван соответствующий visit-метод для этого ребенка.
Задание: напишите visit-методы для узлов Const и Var.
Теперь напишем вывод текстового представления декларации VarDecl. Для этого мы снова воспользуемся функцией txt. Напомним правило грамматики для декларации переменной:
var_decl ::= type <ID< "=" expr ";" ; |
Выражение-параметр функции txt почти в точности воспроизводит это правило:
public void visitVarDecl( VarDecl node ) throws StgException { txt( "${type} ${id} = ${expr};" ); nl(); } |
Здесь появилась еще одна функция – nl(). Она осуществляет перевод строки (сокращение nl расшифровывается как new line).
Задание: напишите visit-методы для узлов Assign и Output. Указание: воспользуйтесь соответствующими правилами грамматики.
Далее напишем вывод текстового представления выражения Expr. Нам нужно вывести список аргументов разделяя их плюсами. Для этого мы воспользуемся библиотечной функцией list:
public void visitExpr( Expr node ) throws StgException { list( "i", 0, node.sizeArgs(), "${args[i]}", " + " ); } |
Первый аргумент функции list – имя итерационной переменной, второй аргумент – начальное значение итерационной переменной, третий аргумент – длина выводимого списка, четвертый аргумент – форматированная строка, содержащая обращение к очередному элементу списка ${args[i]}, наконец, пятый аргумент – разделитель элементов списка.
Нам осталось написать вывод текстового представления узлов: Type, IntType, DoubleType, Arg, Stmt и Program.
Для узлов Type, Stmt и Arg методы будут пустые, так как это абстрактные узлы:
public void visitType( Type node ) throws StgException {} public void visitArg( Arg node ) throws StgException {} public void visitStmt( Stmt node ) throws StgException {} |
Задание: Напишите методы для узлов IntType и DoubleType,
которые должны отвечать за вывод в текстовый файл названий типов int и double соответственно.
Рассмотрим теперь узел Program. В нем нам нужно просто вывести список инструкций. Далее, для вывода полученного списка нам хотелось бы воспользоваться библиотечной функцией list(...):
list( "i", 0, node.getStmts().size(), "${stmts[i]}", "" ); |
Кроме того здесь можно вывести список идентификаторов всех семантических правил, которые применялись при построении очередного дерева. Для этого надо воспользоваться полем semTree унаследованным от VisitorPrinter. Данное поле хранит дерево внутреннего представления и много другой дополнительной информации в частности список примененных семантических правил. Мы написали код для того чтобы оформить список идентификаторов семантических правил в виде комментариев в заголовке теста. Идентификатор первичного семантического правила, для проверки которого строился тест, будет помечен словом MAIN.
Вот так выглядит вывод текстового представления для узла Program:
public void visitProgram( Program node ) throws StgException { //объявим переменную для формирования комментариев String commentStr = ""; //определим количество примененных сем. правил int fullListSize = semTree.getFullSemRelList().size(); //рассмотрим поочередно идентификаторы всех семантических правил for (int i = 0; i < fullListSize;i++) { //рассмотрим i-ую семантическую связь SemanticRelation currRel = semTree.getFullSemRelList().getSemList(i); //сформируем комментарий, содержащий идентификатор семантического правила String currComment = "\n /*" + (currRel.getSrd().equals(semTree.getMainRelation())? " MAIN ":"") + currRel.getSrd().getId() + "*/ \n"; //отбросим повторяющиеся идентификаторы if(commentStr.indexOf(currComment)==-1) { commentStr = commentStr + currComment; } } //выведем получившуюся строку комментариев txt(commentStr); list( "i", 0, node.getStmts().size(), "${stmts[i]}", "" ); } |
В первую очередь следует создать STG-проект. STG проект представляет собой набор следующих файлов:
§ TreeDL-описание грамматики целевого языка;
§ SRL-описание правил статической семантики;
§ SSCL-описание семантических ограничений на синтаксис[5];
§ java-класс, реализующий текстовое представление TreeDL-узлов;
§ файл srl.properties, содержащий список целей генерации (пример приводится в Приложении A);
§ файл stg.properties, содержащий параметры генерации (пример приводится в Приложении B);
§ файлы treedl.plugins и treedl.properties, содержащие настройки для трансляции TreeDL-описания[6] (пример содержимого treedl.plugins и treedl.properties для STG-проекта приводится в Приложении С);
§ файл project.xml, который необходим для формирования проекта для Maven[7] (пример приводится в Приложении D);
§ файл project.properties, содержащий свойства Maven-проекта (пример приводится в Приложении E).
На платформах под управлением ОС семейства UNIX в командном интерпретаторе в директории, содержащей STG-проект, нужно выполнить команду
> stg calc.tdl calc.srl calc.sscl Calc TestsDir
Для генерации тестов на платформах под управлением ОС семейства Windows необходимо запустить командный интерпретатор, перейти в нем в директорию, содержащую STG-проект, и выполнить команду
> stg.bat calc.tdl calc.srl calc.sscl Calc TestsDir
В результате в директории, содержащей STG-проект, сгенерируется директория TestsDir, с тестами. По умолчанию тесты создаются в текущей директории.
Тесты имеют следующую структуру. Если в TreeDL-описании был указан параметр all_tests_file_name, тогда все тесты будут сгенерированы в файл с указанным именем, который будет располагаться по следующему адресу:
<указанная директория для тестов>\1\<all_tests_file_name>
Если в TreeDL-описании не был указан параметр all_tests_file_name, тогда все тесты будут хранится следующим образом:
<указанная директория для тестов> \ <PrimarySemRelID>(<TargetType>) <nid> \ <tid>\Test<ttid>\
Здесь используются следующие обозначения:
§ <PrimarySemRelID> - идентификатор семантического правила, заданный в *.srl и присутствующий в srl.properties в строке <PrimarySemRelID>=true;
§ <TargetType> - тип цели семантического правила <PrimarySemRelID>, указанный в *.srl или неабстрактный тип, потомок указанного типа цели;
§ <nid> - целочисленный идентификатор, начинающийся с единицы, используется для того, чтобы различать созданные во время разных сессий генерации тесты, направленные на тестирование одной и той же ситуации;
§ <tid> - целочисленный идентификатор, начинающийся с единицы, используется для того, чтобы в одной директории не хранить больше 1000 тестов;
§ Test<ttid> - целочисленный идентификатор, начинающийся с единицы, используется для формирования уникального имени для директории, содержащий один тест.
Таким образом, в результате генерации тестов для языка Calc появится тест по следующему адресу:
TestsDir\UniqVarName(VarDecl) 1\1\Test1
Это тест предназначен для тестирования семантического правила, имеющего спецификацию в calc.srl с идентификатором UniqVarName, и примененного для синтаксической конструкции, которой соответствует декларация VarDecl в calc.tdl.
# UniqVarName = true ExistenceOfVarDeclaration_2_1 =
true ExistenceOfVarDeclaration_2_2 =
true SelfInitInVarDeclaration = true ExprType_Double = false AssignTypeCompatibility = true AssignTypeCompatibility_2 = true AssignTypeCompatibility_3 = true |
В srl.properties следует указывать идентификаторы тех семантических правил, для которых требуется сгенерировать тесты. Строка <идентификатор сем. правила> = true означает, что для указанного правила тесты нужны, <идентификатор сем. правила> = false, говорит о том, что для данного правила тесты не надо генерировать. Также тесты не будут генерироваться для правил, которые вообще не указаны в srl.properties.
Комментарии
вводятся символом #.
count_tests = false iter_depth = 5 recur_depth = 3 tests_number = 15 list_size = 2 |
Здесь параметры означают следующее:
Имя параметра |
Значение |
Смысл |
Значение по умолчанию |
count_tests |
true |
Генератор только производит подсчет кол-ва тестов без перевода их в текст |
false |
false |
Генератор работает в обычном режиме |
||
iter_depth |
>=0 |
Задает глубину, ниже которой узлы дерева итерироваться не будут, вместо этого итератор будет возвращать всегда один и тот же объект. Глубина 0 означает, что итерировать требуется только узел-корень дерева, т.е. будут меняться значения только атрибутов этого узла. |
0 |
recur_depth |
>0 |
Задает разрешенное количество узлов одного типа, расположенных в одной цепочке от корня дерева до кроны. |
1 |
tests_number |
>0 |
Задает ограничение на количество тестов, которые будут сгенерированы для каждого правила, указанного в srl.properties. |
без ограничений |
list_size |
>0 |
Задает ограничение на длину списков детей при итерировании. Не является запретом на существование в дереве списков большей длины. |
1 |
Комментарии вводятся символом #.
# actions dump
com.unitesk.atp.tree.tool.DumpAction check
com.unitesk.atp.treedl.CheckAction xref com.unitesk.atp.treedl.PrintHTMLAction translate
com.unitesk.atp.treedl.TranslateAction visitor
com.unitesk.atp.treedl.VisitorAction creator
com.unitesk.atp.treedl.CreatorAction # stg files generators stgnodegen
ru.ispras.redverst.stg.tdl_plugins.StgNodesGenerateAction visgen
ru.ispras.redverst.stg.generator.StgVisitorGenerateAction nitergen
ru.ispras.redverst.stg.generator.StgNodeIteratorGenerateAction # language descriptions java com.unitesk.atp.treedl.JavaLanguageDescription # file generators java_translate com.unitesk.atp.treedl.JavaNodeGenerator java_visitor_empty
com.unitesk.atp.treedl.JavaEmptyVisitorGenerator java_visitor_copy
com.unitesk.atp.treedl.JavaCopyVisitorGenerator java_creator_interface
com.unitesk.atp.treedl.JavaInterfaceCreatorGenerator java_creator_null
com.unitesk.atp.treedl.JavaNullCreatorGenerator java_creator_node
com.unitesk.atp.treedl.JavaNodeCreatorGenerator |
Данный файл содержит список плагинов TreeDL[8]. В разделе «stg files generators» приводится список плагинов, которые поставляются вместе с дистрибутивом STG и необходимы для правильной работы генератора.
Название плагина |
Назначение |
stgnodegen |
Отвечает за трансляцию TreeDL-описания в java-классы, предназначенные для STG-генерации |
visgen |
Отвечает за генерацию
визитеров, которые перечислены в файле treedl.properties |
nitergen |
Отвечает за генерацию итераторов TreeDL-узлов |
Комментарии вводятся символом #.
srl.output.dir src/java sscl.output.dir src/java treedl.visgen.output.dir src/java treedl.stgnodegen.output.dir src/java treedl.nitergen.output.dir src/java treedl.stgnodegen.update false treedl.visgen.update false treedl.nitergen.update false treedl.visgen.childReceiverVisitor true treedl.visgen.childAdderVisitor true treedl.visgen.copierNodeWithAttributeVisitor true treedl.visgen.copierTreeKeepSemanticsVisitor false treedl.visgen.expTypeCounterVisitor true treedl.visgen.nodeSeekerVisitor
true treedl.visgen.nodeCreatorVisitor true treedl.visgen.oneBranchLocationVisitor true treedl.visgen.printerVisitor false treedl.visgen.semanticPrinterVisitor true treedl.visgen.syntaxCompleteVisitor true treedl.visgen.treeCreatorVisitor true treedl.visgen.treeStructPrinterVisitor true treedl.visgen.constValueGetterVisitor false treedl.visgen.expresTypeChangerVisitor true treedl.visgen.descenListReceiverVisitor true treedl.visgen.nodeIteratorReceiverVisitor true treedl.visgen.exchangeChildVisitor true treedl.visgen.multiWayChildAdderVisitor true treedl.visgen.attributeValueSetVisitor true treedl.visgen.deleteNodeVisitor true |
Данный файл в основном содержит список параметров TreeDL-плагинов, перечисленных в файле treedl.plugins. Также в этом файле можно указать некоторые параметры для SRL-трансляции.
Здесь параметры означают следующее:
Имя параметра |
Значение |
Смысл |
Значение по умолчанию |
srl.output.dir |
src/java |
Путь, по которому должны лежать java-классы ¾ результат трансляции srl-описания. Должен совпадать с путем указанным в
теге <sourceDirectory> в соответствующем project.xml. |
Параметр обязательный |
sscl.output.dir |
src/java |
||
treedl.visgen.output.dir |
src/java |
||
treedl.stgnodegen.output.dir |
src/java |
||
treedl.nitergen.output.dir |
src/java |
||
treedl.stgnodegen.update |
false |
В случае значения true, действия за которые отвечает плагин stgnodegen (visgen, nitergen
соответственно) будут производиться только в том
случае, если был изменен файл *.tdl |
|
treedl.visgen.update |
false |
||
treedl.nitergen.update |
false |
||
treedl.visgen.childReceiverVisitor |
true |
Будет сгенерирован визитер, отвечающий за получение
списка детей |
Параметр обязательный |
treedl.visgen.childAdderVisitor |
true |
Будет сгенерирован визитер, отвечающий за
добавление узлов в поддерево |
|
treedl.visgen.copierNodeWithAttributeVisitor |
true |
Будет сгенерирован визитер, позволяющий копировать
узлы |
|
treedl.visgen.nodeSeekerVisitor |
true |
Будет сгенерирован визитер, позволяющий найти узел в
дереве |
|
treedl.visgen.printerVisitor |
false |
Будет сгенерирован визитер, заготовка для
формирования визитера, реализующего перевод тестов в текстовое представление |
|
treedl.visgen.syntaxCompleteVisitor |
true |
Будет сгенерирован визитер, отвечающий за
достраивание дерева до синтаксически полного |
|
treedl.visgen.treeCreatorVisitor |
true |
Будет сгенерирован визитер, позволяющий создать узел
в дереве |
|
treedl.visgen.expresTypeChangerVisitor |
true |
Будет сгенерирован визитер, отвечающий за выполнение
действия над атрибутами, соответствующего compatible |
|
treedl.visgen.descenListReceiverVisitor |
true |
Будет сгенерирован визитер, позволяющий сформировать
список всех узлов в некотором поддереве |
|
treedl.visgen.multiWayChildAdderVisitor |
true |
Будет сгенерирован визитер, который позволяет определить
все возможности присоединения некоторого узла к другому в качестве дочернего |
|
treedl.visgen.attributeValueSetVisitor |
true |
Будет сгенерирован визитер, который отвечает
установку новых значений атрибутов и за изменение целых поддеревьев |
|
treedl.visgen.deleteNodeVisitor |
true |
Будет сгенерирован визитер, который отвечает за
удаление узлов в дереве |
Комментарии вводятся символом #.
<?xml version="1.0" encoding="ISO-8859-1"?> <project>
<pomVersion>3</pomVersion> <id>stg_langdep</id> <name>stg_langdep</name> <groupId>stg</groupId>
<currentVersion>2.0.3</currentVersion> <dependencies> <dependency> <groupId>antlr</groupId> <artifactId>antlr</artifactId> <version>2.7.4</version> </dependency> <dependency> <groupId>atplib</groupId>
<artifactId>atplib</artifactId> <version>3.0</version> </dependency> <dependency> <groupId>treedl</groupId> <artifactId>treedl</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>iterator</groupId>
<artifactId>iterator</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>stg</groupId>
<artifactId>stg_lib</artifactId>
<version>draft-2.0</version> </dependency> </dependencies> <build>
<sourceDirectory>src/java</sourceDirectory> <resources> <resource> <directory>src/conf</directory> <includes>
<include>*.properties</include> </includes> </resource> </resources> </build> </project> |
ВАЖНО! Текст, выделенный полужирным шрифтом, изменять нельзя.
maven.compile.source=1.4 maven.compile.target=1.4 |
Это файл содержит установки необходимые для нормальной работы STG, если на платформе установлен язык java 5.0.
Здесь приводится полный текст TreeDL-описания для рассматриваемого примера
[ translate.language = "java"; visitor.name =
"CalcVisitor"; startnode.name =
"Program"; target.language =
"Calc"; target.extension =
"clc"; all_tests_file_name =
"calc.tests"; ] tree ru.ispras.redverst.stg.langdep.modl.CalcTree :
<com.unitesk.atp.tree.TreeClass>; node Program : <BaseNode> { child Stmt+ stmts; } abstract node Stmt : <BaseNode> {} node VarDecl : Stmt { child Type type; child Id id; child Expr expr; } node Assign : Stmt { child Id assId; child Expr assExpr; } node Output : Stmt { child Expr expr; } abstract node Type : <TypeDesc> {} node IntType : Type {} node DoubleType : Type {} node Expr : <BaseNode> { child Arg+ args; child Type type; } abstract node Arg : <BaseNode> { child Type type; attribute <Integer>
value; } node Const : Arg { body { public Object
newValue() { if (this.type instanceof IntType) { return new Integer((value.intValue()>=Byte.MAX_VALUE
|| value.intValue()<=Byte.MIN_VALUE
? value.intValue() : Byte.MAX_VALUE - 10)); } if (this.type instanceof DoubleType) { return new
Double("1.00000001" + value.intValue()); } throw new
IllegalArgumentException("type"); } public String toString() { return newValue().toString(); } } } node Var : Arg { child Id varId; } node Id : <BaseNode> { attribute <String>
name; } |
Далее приводится полный текст SRL-описания для языка Calc.
many_to_many relation UniqVarName { comment "UniqVarName" arbitrary unequal target VarDecl { id } source VarDecl { id } context c1: same Program } one_to_many relation ExistenceOfVarDeclaration_2_1 { comment "Existence of
var declaration for left part of assignment" semantic equal target Assign { assId } source VarDecl { id } context c1: same Program } one_to_many relation ExistenceOfVarDeclaration_2_2 { comment "Existence of
var declaration for var use in expression" semantic equal target Var { varId } source VarDecl { id } context c1: same Program } many_to_many relation SelfInitInVarDeclaration { comment "In var
declaration var must not be initialized by itself" arbitrary unequal target this.target_context
{ id } source Var { varId } context c1: same VarDecl } many_to_many relation ExprType_Double { comment "Expression
type is defined by the type of its arguments: if expr is of type Double it
means that there is at least one double argument in this expression" arbitrary present target this.target_context
{ type[is DoubleType] } source Arg { type[is DoubleType] } context c1: same Expr } one_to_one relation AssignTypeCompatibility { comment "The type of
assignment right part must be compatible to the type of the assignment left
part" arbitrary compatible target Expr { type } source
this.target_context { type } context c1: same VarDecl } one_to_one relation AssignTypeCompatibility_2 :
ExistenceOfVarDeclaration_2_1 { comment "The type of
assignment right part must be compatible to the type of the assignment left
part" arbitrary compatible target Expr { type } source
this.target_context.ExistenceOfVarDeclaration_2_1[target = prev].source {type } context c1: same Assign } one_to_one relation AssignTypeCompatibility_3 :
ExistenceOfVarDeclaration_2_2 { comment "The type of
var is determined by variable declaration" arbitrary equal target Var { type } source this.target.ExistenceOfVarDeclaration_2_2[target
= prev].source {type } context c1: same Program } type_set PrimitiveTypes { ref IntType, ref DoubleType } |
Далее приводится полный текст java-класса, реализующего перевод тестов для языка Calc в текстовое представление.
package ru.ispras.redverst.stg.langdep.visitors; import ru.ispras.redverst.stg.base.BaseNode; import
ru.ispras.redverst.stg.base.visitors.VisitorPrinter; import
ru.ispras.redverst.stg.exceptions.StgException; import
ru.ispras.redverst.stg.langdep.modl.CalcTree.*; import
ru.ispras.redverst.stg.semrel.SemanticRelation; public class
CalcVisitorPrinter extends VisitorPrinter implements CalcVisitor { public java.util.List start( BaseNode node
) throws StgException { return new java.util.Vector(); } public void visitId( Id node ) throws
StgException { txt( "${name}" ); } public void visitVarDecl( VarDecl node )
throws StgException { txt( "${type} ${id} =
${expr};" ); nl(); } public void visitConst( Const node )
throws StgException { txt(node.toString()); } public void visitVar( Var node ) throws
StgException { txt( "${varId}" ); } public void visitAssign( Assign node )
throws StgException { txt( "${assId} = ${assExpr};"
); nl(); } public void visitOutput(
Output node ) throws StgException { txt( "print ${expr};" );
nl(); } public void visitExpr( Expr node ) throws
StgException { list( "i", 0,
node.sizeArgs(), "${args[i]}", " + " ); } public void visitType( Type node ) throws
StgException {} public void visitArg( Arg node ) throws
StgException {} public void visitStmt( Stmt node ) throws
StgException {} public void visitDoubleType(DoubleType
node) throws StgException { txt("double "); } public void visitIntType(IntType node)
throws StgException { txt("int "); } public void visitProgram( Program node )
throws StgException {
//объявим переменную для формирования комментариев
String commentStr = "";
//определим количество примененных сем. правил int
fullListSize = semTree.getFullSemRelList().size();
//рассмотрим поочередно идентификаторы всех семантических правил for (int i = 0; i < fullListSize;i++) { //рассмотрим i-ую семантическую связь SemanticRelation currRel =
semTree.getFullSemRelList().getSemList(i); //сформируем комментарий, содержащий идентификатор
семантического правила
String currComment = "\n /*" +
(currRel.getSrd().equals(semTree.getMainRelation())? " MAIN
":"") + currRel.getSrd().getId() + "*/ \n"; //отбросим повторяющиеся идентификаторы
if(commentStr.indexOf(currComment)==-1) { commentStr = commentStr
+ currComment; } } //выведем получившуюся строку комментариев txt(commentStr); list( "i", 0,
node.getStmts().size(), "${stmts[i]}", "" ); } } |
[1] Если этот параметр не указан, тогда все тесты будут записаны в отдельные файлы.
[2] Ответ для этого задания и для всех дальнейших приводятся в Приложении F.
[3] Подробнее о ссылках на объекты правил можно прочитать в “STG User Guide”.
[4] Правила форматирования строк приводятся в TreeDL документации. Здесь лишь отметим следующее: для того чтобы вызвать метод визитера для дочернего узла нужно поместить имя этого узла в фигурные скобки и поставить перед ними символ $, например txt(“${name}”). Вызов метода, например toString(), который возвращает String, нужно просто вставлять в txt в качестве параметра, например txt(node.toString()).
[5] Если нет семантических ограничений на синтаксис, тогда в проекте должен присутствовать пустой файл с расширением *.sscl.
[6] Настройки TreeDL-трансляции описаны в документации к TreeDL.
[7] Правила оформления конфигурационного файла project.xml для Maven-проекта приводятся в документации к Maven.
[8] Подробное описание реализации, подключения и запуска TreeDL-плагинов приводится в TreeDL-документации.