Внутреннее представление программы с языка Фортран представляет собой AST (абстрактное синтаксическое дерево). Библиотека Sage++ представляет собой реализацию данного дерева разбора. Все высокоуровневые функции, структуры и классы в Sage++ начинаются с префикса Sg, за исключением некоторых функций, которые проверяют некоторые свойства, например, isSgForStmt(SgStatement*) - функция, проверяющая является ли выбранный узел циклом или нет.
Внутреннее представление создается с помощью парсера. Парсер использует грамматику для синтаксического анализа. Данная грамматика расширена для поддержки директив DVMH и SAPFOR. Для того, чтобы начать работать с внутренним представлением, необходимо загрузить в память все файлы проекта, созданные парсером. Это делается с помощью класса SgProject, в конструктор, которому передается имя текстового файла, в котором перечислен список *.dep файлов. После создания проекта можно использовать следующий уровень абстракции - файл. Файл является единицей трансляции, поэтому для того, чтобы переключиться на конкретный файл, необходимо использовать следующий вызов: SgFile *file = &(project.file(0)); - данная функция получает файл с индексом 0. Также, класс SgProject содержит в себе весь необходимый функционал для работы с проектом, например, количество файлов в проекте ( project.numberOfFiles() ), или имя каждого файла в проекте ( project.fileName(0) ). Таким образом, стандартная фаза компиляции представляет из себя проход по всем файлам для анализа кода программы пользователя.
В текущей реализации SAPFOR уже определен механизм проходов, открытия проекта и выбора файлов. Каждый проход (фаза анализа) может обрабатывать каждый файл отдельно, а также, если необходима агрегация результатов анализа, может иметь функцию, которая работает после обработки всех файлов проекта.
Каждый файл проекта представляет из себя набор связанных между собой операторов или SgStatement. Данный класс является абстрактным представлением всех операторов (базовым классом), от которого наследуются остальные классы для реализации специальных возможностей, присущие отдельно взятому оператору. Например, SgForStmt, является производным классом от SgStatement и содержит весь необходимый функционал для работы с оператором цикла. Согласно правилам языка С++, любой производный класс может быть приведен к базовому классу.
В дереве разбора пользователь "видит" только SgStatement. К примеру, заголовок функции, оператор присваивания или оператор цикла - все это содержит в себе SgStatement. У каждого такого оператора есть вариант ( SgStatement->variant() ), который и задает вид конкретного оператора. Узнав вариант рассматриваемого оператора, можно использовать производное представление данного класса, преобразовав базовый класс к производному. Гарантируется, что данный оператор с правильным вариантом содержит всю необходимую информацию для производного класса. Например, чтобы узнать, является ли данный оператор оператором цикла, можно использовать следующий код:
SgStatement *st = currSt;
if (st->variant() == FOR_NODE)
SgForStmt *forSt = (SgForStmt*) st;
либо можно использовать такой код:
SgStatement *st = currSt;
SgForStmt *forSt = isSgForStmt(st);
if (forSt)
DoSomth();
Все варианты ( *->variant() ) описаны в файле tag и dvm_tag.h. Каждый узел обязан иметь какой-либо вариант. Помимо класса SgStatement, есть класс SgExpression, представляющий собой выражения. Данный класс реализует выражения, которые есть у операторов. Например, следующий оператор присваивания:
A[i] = B[i] + C[i]
содержит в себе два выражения - то, что находится слева от оператора присваивания, и то, что находится справа. Для того, чтобы получить доступ к этим выражениям, необходимо использовать соответствующие функции у класса SgStatement: например, expr(N) позволяет получить выражение N. Если выражения с номером N не существует, то вернется пустой указатель (NULL). Всего оператор может содержать не более трех выражений (то есть N = 0, 1, 2). Данные выражения представляю собой SgExpression, которыми наполняется оператор.
Класс SgExpression также является базовым классом для представления выражений. У данного класса есть такая же функция для взятия варианта ( SgExpression->variant() ). Используя данную функцию, можно узнать, с каким именно выражением необходимо работать и выполнить соответствующее преобразование к производному. Способ преобразования и проверки для выражений такой же, как и для операторов (см. пример выше).
Все операторы файла (SgStatement) связаны между собой, есть понятие следующего оператора за данным в лексическом порядке, и предыдущего оператора перед данным в лексическом порядке. Также у каждого оператора есть родительский оператор, который задает уровни вложенности операторов. Таким образом, следующий оператор, который идет за данным, не обязательно должен принадлежать текущей области вложенности (иметь одного и того же родителя). Например,
if (condition) then
op1
op2
endif
op3
за первым оператором, лексически следует оператор 2, за вторым - конец IF блока, а за концом IF блока - оператор 3. Узнать, какой оператор является родителем для данного оператора, можно с помощью функции controlParent().
Стоит отметить оператор с вариантом CONTROL_END. Данный оператор определяет конец блока операторов языка фортран, например, END IF, END DO, END FUNCTION и т.д. Для определения родителя для данного оператора нужно использовать функцию controlParent(). У каждого оператора есть функция определения последнего оператора для данного - lastNodeOfStatement(). Если оператор является составным, например, IF – END IF, то последним оператором будет ENDIF с вариантом CONTROL_END.
В отличие от операторов, выражения связаны в правое рекурсивное двоичное дерево. У каждого узла есть левый потомок ( SgExpression->lhs() ) и/или правый потомок ( SgExpression->rhs() ). Каждый потомок также является SgExpression. У какого узла может быть только левый потомок, либо только правый, либо вообще может не быть потомков (в данном случае соответствующие функции вернут пустой указатель). Для работы с такими деревьями требуется понимание рекурсии и двоичного дерева. Рекурсивно обойти такое дерево из выражений можно, например, следующим образом:
static void recExpression(SgExpression *exp, const int lvl) {
if (exp) {
SgExpression *lhs = exp->lhs();
SgExpression *rhs = exp->rhs();
doSmth()
recExpression(lhs, lvl + 1);
recExpression(rhs, lvl + 1);
}
}
У каждого оператора и выражения есть возможность получения его исходного кода на языке Фортран, то есть можно выполнить генерацию кода отдельного взятого оператора и выражения. Соответствующая функция называется unparsestdout(). Данная функция позволяет выполнить генерацию кода в консоль. Она служит в основном для отладки. Стоит заметить, что если вызвать данную функцию для оператора "Функция" или "IF блок", то вместе с этим оператором будут сгенерированы все вложенные операторы в данный (или все те операторы, у которых родитель - данный оператор). Аналогично и для выражений - будет сгенерирован код для всего бинарного дерева, начиная от текущего узла и ниже.
Для удобства отладки, в SAPFOR есть функция recExpressionPrint (SgExpression *exp);, которая позволяет получить наглядное представление бинарного дерева разбора для выражений в формате GraphViz, именуются узлы графа по такому правилу: NODENUM_LVL_LR_TAGNAME_VALUE:
- NODENUM - номер узла,
- LVL - глубина узла в дереве,
- LR - левое или правое это поддерево,
- TAGNAME - имя варианта, соответствующее предопределенным макросам в файлах tag и dvm_tag.h,
- VALUE - значение, которое было в исходном коде, если оно доступно (например, имя символа, функции или операции).
Данная функция на начальном этапе может существенно упростить процесс отладки и понимания того, как устроено внутреннее представление.
Рассмотрим такой оператор: B(I, J, K) = A(I, J, K-1) + A(I, J-1, K) + A(I-1, J, K). Для него с помощью функции recExpressionPrint() можно получить представление для левого выражения (стоящего слева от присваивания) и для правого. Данная функция (recExpressionPrint) выводит код для GraphViz в стандартный поток вывода (консоль). Код для визуализации правого выражения будет выглядеть так:
digraph G{
"0_0_L_ADD_OP_(+)" -> "1_1_L_ADD_OP_(+)";
"0_0_L_ADD_OP_(+)" -> "2_1_R_ARRAY_REF_(a)";
"1_1_L_ADD_OP_(+)" -> "3_2_L_ARRAY_REF_(a)";
"1_1_L_ADD_OP_(+)" -> "4_2_R_ARRAY_REF_(a)";
и т.д.
};
Визуализировать данный код можно с помощью Web GraphViz по этой ссылке или по этой ссылке , либо можно скачать с сайта по последней ссылке программу для визуализации графов. По построенному графу легко сопоставить исходное выражение с внутренним представлением.
Помимо операторов и выражений есть таблицы символов (SgSymbol) и типов (SgType), которые свои для каждого файла и общие для всех операторов и выражений в данном файле. Символы представляют собой наполнение выражений и операторов. Например, в приведенном выше выражении, есть символы A, B - которые являются символами следующего типа: трехмерный массив из double (базовый тип double, производный - массив из трех измерений), а символы I, J, K являются символами с типом Integer. В данном выражении для всех обращений к I, J, K будет ссылка на таблицу символов к единственным экземплярам I, J, K. Таблицы символов и типов доступны по соответствующей функции класса SgFile. На базе встроенных типов можно строить производные типы. Таблица типов доступна на уровне файла.
На текущий момент существует две версии библиотеки Sage++. Компилятором FDVMH и SAPFOR используется первая версия. На сайте Sage++ также доступна вторая версия. Описание классов, приведенных на сайте, может отличаться в зависимости от версии. По этой ссылке доступна некоторая документация и иерархия классов первой версии, а также доступно более наглядное представление всех интерфейсов второй версии.
Некоторые примеры можно найти на сайте этой библиотеки. Также можно рассмотреть некоторые простые проходы, реализованные в SAPFOR, которые описаны ниже.
Так как данная библиотека разрабатывалась в зарубежных университетах в том числе студентами, можно встретить какие-то некорректности или ошибки. Периодически мы стараемся вносить изменения и исправлять некоторые ошибки и некорректности как на высоком уровне, так и на низком. Также в интерфейс Sage++ можно добавлять некоторые возможности, которые могут упростить его использование. Изменение уже существующих классов крайне не рекомендуется и в большинстве случаев не практикуется, а расширение функциональности - наоборот приветствуется.