-
Notifications
You must be signed in to change notification settings - Fork 18
Program Transformation
- Введение
- Распараллеливание на общую память
- Подстановка процедур
- Распространение выражений
- Замена структурных типов данных
Инструмент TSAR обеспечивает выполнение различных преобразований исходного кода программы. Выбирать преобразования, а также управлять их выполнением можно, используя опции анализатора, доступные из командной строки, и вставляя в исходный код программы директивы специального вида.
Изменения вносятся в исходные файлы программы, а копии оригинальных версий файлов сохраняются в файлах вида <filename>.<ext>.orig
. Чтобы не изменять исходные файлы, а сохранить изменения в их копиях можно воспользоваться опцией -output-suffix=<suffix>
. В этом случае модифицированная версия файла <filename>.<ext>
сохраняется в файле <filename>.<suffix>.<ext>
.
Преобразование программ на языках C/C++ основано на возможностях Clang, директивы при этом имеют вид #pragma spf ...
. По умолчанию, после преобразования программы выполняется форматирование преобразованных файлов, основанное на clang-format
. При форматировании используется стиль LLVM
. Изменить данное поведение, отключив форматирование, можно с помощью опции -no-format
.
У каждого преобразования есть своя область применимости и ограничения на допустимость его выполнения. Ограничения, общие для всех преобразований - следующие:
- не допускается преобразование системных файлов;
- в большинстве случаев не допускается преобразование, если в преобразуемой области используются макро определения или содержаться директивы препроцессора, отличные от
#pragma
; - не допускаются преобразования внутри макро конструкций.
В случае невозможности выполнить преобразование выдается предупреждение. Тот факт, что выдается предупреждение, а не ошибка вытекает из того, что невозможность выполнить одно из запрошенных преобразований, не препятствует преобразованию других участков кода.
Остановимся на проблеме преобразования программ, использующих макро конструкции более подробно. Использование макросов непредсказуемым образом меняет поведение программы. В общем случае, считается, что преобразованная программа должна компилироваться с теми же макро определениями, с которыми выполнялось преобразование (набор опций командной строки с префиксом -D
). Но даже в этом случае использование макросов значительно затрудняет преобразование.
Во-первых, возможности Clang не всегда позволяют найти все макросы, используя синтаксически абстрактное дерево (AST). Например, макро определению с пустым телом #define M
ничего не соответствует в AST. Чтобы предотвратить некорректные преобразования, в TSAR дополнительно выполняется консервативный поиск конструкций, похожих на макросы, в текстовом представлении исходного кода программы. Это в свою очередь может привести к необоснованному отказу от выполнения преобразований. В текущей реализации отключить такое поведение невозможно.
Во-вторых, некоторые преобразования (например, подстановка процедур) могут приводить к тому, что конструкция, которая макросом не являлась, станет макросом после преобразования:
int M;
void foo() { M = 1; }
#define M 2
void bar() {
#pragma spf transform inline
foo();
}
В рассмотренном примере в случае подстановки функции foo()
на место ее вызова идентификатор M
превратится из имени глобальной переменной в имя макроса. Так как отследить такую ситуацию без анализа уже преобразованной программы и построения AST крайне сложно, а проверка только по исходному коду дает слишком консервативные результаты и препятствует многим допустимым преобразованиям, было принято решение не считать такую ситуацию препятствующей преобразованию. Вместо этого преобразованная область окружается директивой assert nomacro
, которая позволяет выполнить данную проверку уже после преобразования, используя опцию командной строки -check
, доступную в TSAR. Результат преобразования рассмотренного примера приведен ниже:
int M;
void foo() { M = 1; }
#define M 2
void bar() {
#pragma spf assert nomacro
/* foo() is inlined below */
{
M = 1;
}
}
Предупреждение. При выдаче диагностических сообщений указывается только первая проблема, препятствующая преобразованию.
После выполнения преобразований описывающие их директивы удаляются из исходного кода программы, если это безопасно. Это сделано для того, чтобы избежать случайного выполнения преобразований при повторном запуске анализатора для уже преобразованной программы.
Распараллеливание программ с целью их последующего выполнения на параллельных вычислительных системах с общей памятью заключается во вставке соответсвующих спецификаций параллелизма, возможно с предварительной модификацией исходного кода программ. Поддерживается распараллеливание для мультипроцессора с применением OpenMP (опция -clang-openmp-parallel
), а также для мультипроцессора и графических ускорителей в модели DVMH (опция -clang-dvmh-sm-parallel
).
По умолчанию распараллеливание выполняется для всей программы с реализацией параллелизма уровня циклов. В случае если необходимо выполнить распараллеливание только некоторых участков кода, то данные участки кода можно выделить директивой #pragma spf region
. При необходимости можно использовать необязательную спецификацию name
, чтобы указать задать имя для выделенного участка кода (можно задать одинаковое имя для нескольких участков кода). Если в программе выделен хотя бы один участок кода, то распараллеливание будет выполняться только внутри данного участка. Если используются именованные участки кода, то опция -foptimize-only
позволяет задать список имен участков кода, внутри которых должно выполняться распараллеливание.
Данный режим распараллеливание в модели DVMH не предусматривает распределение данных между процессорными устройствами и не предназначен для распараллеливания на вычислительный кластер. Можно выделить 2 основных варианта распараллеливания программы в данном режиме:
- добавление спецификаций регионов DVMH (
#pragma dvm region
), возможно с указанием вычислительных устройств, на которых може быть выполнен конкретный регион (targets
), и спецификаций параллельных циклов (#pragma dvm parallel
) внутри каждого региона; - добавление только спецификаций параллельных циклов (
#pragma dvm parallel
), если указание регионов не возможно по тем или иным причинам; в этом случае данные параллельные циклы не могут быть выполнены на ускорителях, а полученная DVMH программа должна быть скомпилирована с опцией DVMH компилятора-Opl
, что позволит запустить ее на мультипроцессоре.
2.1 Листинг. Пример автоматического распараллеливания программы в модели DVMH для выполнения на мультипроцессоре или графическом ускорителе.
#include <stdio.h>
#define N 10
long A[N][N][2];
int main() {
long S = 0;
#pragma dvm actual(A, S)
#pragma dvm region in(A, S)out(A, S)
{
#pragma dvm parallel([I][J][K]) tie(A[I][J][K])
for (int I = 0; I < N; ++I)
for (int J = 0; J < N; ++J)
for (int K = 0; K < 2; ++K)
A[I][J][K] = I + J + K;
#pragma dvm parallel([I][J][K]) tie(A[I][J][K]) across(A [1:0] [1:0] [0:0])
for (int I = 1; I < N; ++I)
for (int J = 1; J < N; ++J)
for (int K = 0; K < 2; ++K)
A[I][J][K] = A[I - 1][J][K] + A[I][J - 1][K] + A[I][J][K];
#pragma dvm parallel([I][J][K]) tie(A[I][J][K]) reduction(sum(S))
for (int I = 0; I < N; ++I)
for (int J = 0; J < N; ++J)
for (int K = 0; K < 2; ++K)
S += A[I][J][K];
}
#pragma dvm get_actual(A, S)
printf("Sum = %ld\n", S);
return 0;
}
Данное преобразование заменяет вызов процедуры на ее тело, для формальных параметров добавляются объявления соответствующих локальных переменных в месте вызова процедуры. Эти переменные инициализируются значениями фактических аргументов вызова. Если у процедуры есть возвращаемое значение, то для его хранения заводится дополнительная локальная переменная, данная переменная затем используется в выражении, содержащем вызов процедуры, вместо вызова. Результат подстановки при этом помещается перед данным выражением. Чтобы избежать дополнительных безусловных переходов и упростить результат подстановки недостижимые выходы из процедуры (return
) будут удалены при подстановке.
В текущей реализации подстановка процедур возможна для программ на языке C99. Для более поздних стандартов, а также для C++ подстановка возможна в некоторых случаях, но ее корректность не гарантируется.
Замечание. В контексте C-подобных языков понятие процедуры рассматривается как эквивалентное понятию функции.
Подстановка выполняется только для тех вызовов процедур, которые были помечены специальной директивой #pragma spf transform inline
, либо которые находятся внутри составного блока операторов {...}
, помеченного данной директивой.Д ля преобразования программ с раставленными директивами inline
необходимо использовать опцию -clang-inline
.
Предупреждение. Если директива inline
расположена непосредственно перед циклом, условным оператором, оператором switch
, то ее действие распространяется только на заголовок цикла, условного оператора (условие) или оператора switch
соответственно. Чтобы распространить ее действие на весь оператор целиком, необходимо поместить данный оператор внутрь составного блока операторов {...}
и разместить директиву непосредственно перед данным блоком.
В текущей реализации подстановка процедур не будет выполнена в следующих случаях:
- тело подставляемой процедуры недоступно;
- подставляемая процедура определена в системных файлах;
- подставляемая процедура может принимать переменное количество аргументов (variadic function);
- подставляемая процедура является рекурсивной и подстановка ее вызова приведет к зацикливанию TSAR: подстановка рекурсивной процедуры будет выполнена, в том случае, если вызовы из ее тела, приводящие к рекурсии не должны быть подставлены (например, они не помечены директивой
inline
); - подставляемый вызов является недостижимым (unreachable calls, см. issue #36);
- в вызове или в теле подставляемой процедуры содержаться макро конструкции, при этом допускается использование директив внутри тела подставляемой процедуры;
- вызов процедуры или ее определение целиком или частично расположены внутри макро конструкции;
- начало и конец оператора, содержащего вызов, расположены в разных файлах (например из-за использования директив
#include
); - подставляемый вызов расположен внутри заголовочного файла;
- подставляемый вызов содержится внутри тернарного оператора
?:
: для выполнения подстановки тернарный оператор должен быть преобразован к условному оператору; - подставляемый вызов содержится в условии цикла или внутри инкремента for-цикла: в этом случае подставленное тело процедуры должно выполняться на каждой итерации цикла и это потребует дополнительных преобразований;
- подставляемый вызов содержится в правой части логического выражения: при вычислении таких выражений в C/C++ используется укороченная логика, и для корректной подстановки требуется разделить данное выражение на несколько и использовать конструкции, состоящие из условных операторов;
- в подставляемой процедуре используются глобальные переменные, которые в результате выполнения подстановки окажутся скрыты локальными переменными процедуры, из которой выполняется вызов (см. листинг 2.1);
- в подставляемой процедуре используются конструкции (переменные, типы, процедуры), объявления которых не доступны в точке вызова: во многих случаях возможно добавить объявления таких конструкций (forward declaration) в процессе подстановки, но в данный момент такая функциональность не реализована (см. issue #37).
3.1 Листинг. Подстановка невозможна, так как объявление локальной переменной X
скрывает используемую глобальную переменную
int X;
void f() { X = 5; }
void f1() {
int X;
#pragma spf transform inline
f();
}
Данное преобразование заменяет вхождения переменных в исходном коде программы выражениями, вычисляющими значения данных переменных. Подстановка возможно только в том случае, если выражение, вычисляющее значение переменной, доступно в точке подстановки.
В определенных ситуациях подставлены могут быть как значения отдельных переменных, так и полей структур. Допускается подстановка значений переменных, содержащих указатель на функцию. Также допускается распространение выражений, вычисляющих укзатели на отдельные измерения массивов (см. листинги 3.1 и 3.2).
void foo(int N, int M, int I, float (A*)[N][M]) {
#pragma spf transform propagate
float (*B)[M] = A[I];
for (int J = 0; J < N; ++J)
for (int K = 0; K < M; ++K)
B[J][K] = I + J + K;
}
void foo(int N, int M, int I, float (*A)[N][M]) {
#pragma spf assert nomacro
{
float(*B)[M] = A[I];
for (int J = 0; J < N; ++J)
for (int K = 0; K < M; ++K)
(A[I])[J][K] = I + J + K;
}
}
Предупреждение. Рекурсивное распространение выражений не поддерживается. Это означает, что в случае последовательности присваиваний X = Q + 1; Y = X + 1; Z = Y + 1
результатом будет Y = Q + 1; Z = X + 1
. Чтобы выполнить подстановку выражения, вычисляющего X
для Z = X + 1
нужно запустить выполнение преобразования повторно.
В текущей реализации распространение выражений возможно для программ на языке C99. Для более поздних стандартов, а также для C++ преобразование возможно в некоторых случаях, но его корректность не гарантируется.
Задать область выполнения преобразования можно с помощью специальной директивой #pragma spf transform propagate
, размещенной перед составным блоком операторов {...}
. Также с помощью данной директивы можно пометить отдельные операторы объявления переменных. В этом случае заменены будут только объявляемые переменные, а областью применения данного преобразования будет вся область видимости данных переменных. Для преобразования программ с раставленными директивами propagate
необходимо использовать опцию -clang-propagate
.
В текущей реализации распространение доступных выражений не будет выполнено в следующих случаях:
- область подстановки содержит макро конструкции;
- заменяемое обращение переменной расположено внутри заголовочного файла;
- заменяемое обращение переменной расположено внутри системного файла;
- обращение переменной находится в простом присваивании (direct assignment) вида
X = Y
(в данном случае значение переменнойY
не будет подставлено вместо использованияY
в данном присваивании); - объявления переменных, участвующих в распространяемом выражении, не доступны в точке подстановки выражения.
Замечание. Анализ доступных выражения выполняется над LLVM IR после размещения переменных на регистрах (проходы Promote Memory to Register, Scalar Replacement Of Aggregates). Для соотнесения результатов с объектами в исходном коде программы, представленными в виде Clang AST, используется отладочная информация. Таким образом, от полноты отладочной информации и от области применимости используемых проходов LLVM зависит области применимости данного преобразования. В связи с этим возможны отдельные ситуации, в которых преобразование выполнено не будет.
Данное преобразование, выполненное для некоторой процедуры, заменяет ее формальные параметры, являющиеся указателями на объекты структурных типов, на формальные параметры, соответсвующие отдельным элементам структуры, к которым осуществляется доступ в преобразуемой процедуре. При этом создается копия исходной процедуры: тело исходной процедуры (за исключением удаления директив, описывающих преобразование) не изменяется. Если из преобразуемой процедуры осуществляются вызовы других процедур, в которые в качестве фактических параметров передаются заменяемые формальные парметры, то будут созданы соответствующим образом преобразованные копии вызываемых процедур, а соответсвующие вызовы также будут модифицированы. Пример преобразуемого и преобразованного фрагмента программы приведен в листингах 4.1 и 4.2 соответственно.
Замечание. В контексте C-подобных языков понятие процедуры рассматривается как эквивалентное понятию функции.
В текущей реализации преобразование возможно для программ на языке C99. Для более поздних стандартов, а также для C++ подстановка возможна в некоторых случаях, но ее корректность не гарантируется.
enum Kind { K1, K2 };
typedef float T;
struct STy {
struct QTy {
T V1;
T V2;
} * Q;
unsigned N;
enum Kind K;
T R;
T (*F)(T, T);
};
void bar(int I, struct STy *S) {
if (S->K == K1)
S->R = S->F(S->R, S->Q[I].V1);
else
S->R = S->F(S->R, S->Q[I].V2);
}
void foo(struct STy *S) {
for (int I = 0; I < S->N; ++I)
bar(I, S);
#pragma spf transform replace(S) nostrict
}
Задать формальные параметры процедуры, которые должны быть преобразованы можно с помощью специальной директивы #pragma spf transform replace(...)
. Данная директива может быть размещена в любой точке преобразумеой процедуры. Также допускается использование нескольких директив. В этом случае преобразованию будут подвергнуты формальные параметры, упоминаемые хотя бы в одной из директив. Для преобразования программы с расставленными директивами replace
непобходимо использовать опцию -clang-struct-replacement
.
В результате преобразования формальный параметр, указанный в директиве replace
, заменяется на множество формальных параметров, соответсвующих элементам структуры, явно используемым в преобразованной процедуре. В случае, если элементы стуркутры не используются в процедуре, они не будут указаны в качестве формальных параметров для создаваемой процедуры.
Замечание. Если ни один элемент структуры, соответсвующей преобразуемому формальному параметру, не используется внутри процедуры, то данный параметр будет полностью удален из определения новой процедуры.
/* Replacement for void bar(int I, struct STy *S) */
void bar_spf0(int I, enum Kind S_K0, T *S_R0, T (*S_F0)(T, T),
struct QTy *S_Q0) {
#pragma spf metadata replace( \
bar(I, {.K = S_K0, .R = S_R0, .F = S_F0, .Q = S_Q0}))
if (S_K0 == K1)
(*S_R0) = S_F0((*S_R0), S_Q0[I].V1);
else
(*S_R0) = S_F0((*S_R0), S_Q0[I].V2);
}
/* Replacement for void foo(struct STy *S) */
void foo_spf0(unsigned int S_N0, enum Kind S_K1, T *S_R1, T (*S_F1)(T, T),
struct QTy *S_Q1) {
#pragma spf metadata replace( \
foo({.N = S_N0, .K = S_K1, .R = S_R1, .F = S_F1, .Q = S_Q1}))
for (int I = 0; I < S_N0; ++I)
bar_spf0(I, S_K1, S_R1, S_F1, S_Q1);
}
Спецификация nostrict
аналогична указанию квалификатора restrict
при описании формальных параметров процедуры и подразумевает отсутствие косвенных обращений к элементам структуры. В этом случае элементам структуры, значения которых не изменяются внутри преобразуемой процедуры, будут соответсвовать параметры новой процедуры, передаваемые по значению. Например, параметр S_N0
, задающий размер массива S->Q
, имеет тип unsigned int
и соответствует элементу структуры S->N
. Для модифицируемых элементов структуры в качестве соответствующего параметра в новой процедуре будет использован указатель требуемого типа. Например, редукционная переменняая S_R0
, вычисляемая в процедуре будет иметь тип T *
и соответсвовать элементу структуры S->R
.
В случае отсутствия спецификации nostrict
, чтобы гарантировать корректность преобразования, для передачи потенциально изменяемых элементов структуры в новую процедуру, всегда будут использованы указатели соответсвующего типа.
Замечание. В случае использования нескольких директив replace
в теле преобразуемой процедуры, спецификация nostrict
должна быть указана для каждой директивы, иначе данная спецификация будет игнорироваться в процессе преобразвоания.
Дополнительным эффектом от использования спецификации nostrict
является возможность выполнения преобразования при использовании макро конструкций в преобразумеой процедуре. При этом макро конструкции, используемые в описании типов элементов заменяемых структур, будут разавернуты в объявлениях формальных параметров создаваемой процедуры. Макро конструкции, используемые в теле процедуры, развернуты не будут. Новая процедура будет размещена в исходном коде непосредственно после преобразуемой, при этом ответственность за соответствие результата подстановки препроцессором макро конструкций в теле созданной процедуры результату подстановки тех же макро конструкций в теле исходной процедуры ложится на пользователя, запросившего преобразование. Неконсистентность использования макро конструкций может возникнуть, например, из-за использования директив #undef
в теле преобразуемой процедуры.
Описание соответствия между параметрами исходной процедуры и процедуры, получаемой в результате преобразования, будет добавлено в тело новой процедуры в виде директивы #pragma spf metadata replace(...)
. Наличие данной спецификации в теле процедуры позволяет выполнить дальнейшее преобразование программы, заменив вызовы исходной процедуры, помеченные директивой #pragma spf transform replace(...) with(...)
, на вызовы процедуры, получаемой в результате преобразования.
В текущей реализации замена формальных параметров не будет выполнена в следующих случаях:
- тип заменяемого параметр не является указателем на структурный тип данных;
- заменяемая переменная не является формальным параметром процедуры;
- при обращении к структуре и ее элементам используются операции отличные от
->
, за исключением случаев передачи заменяемого параметра в процедуру, которая вызывается из преобразуемой и вызов которой будет также заменен в процессе преобразования; - невозможно описать тип создаваемого формального параметра, например, если в нем используются псевдонимы типов, не доступные в точке описания процедуры;
- определение процедуры целиком или частично расположено внутри макро конструкции;
- обращения к элементам структуры или преобразуемые вызовы процедур расположены полностью или частично внутри макро конструкций и заменяемые фрагменты кода пересекают границы данных макро конструкций;
- начало и конец определения процедуры расположены в разных файлах (например из-за использования директив
#include
); - преобразуемая процедура определена в системных файлах;
- в теле преобразумемой процедуры содержаться макро конструкции и хотя бы в одной из директив, описывающих преобразование, не задана спецификация
nostrict
; при этом допускается использование директив внутри тела подставляемой процедуры.
Замечание. Допускается замена только части параметров, указанных в директиве replace
, для которых выполнены условия допустимости преобрзаования.
Помимо создания измененной копии исходной процедуры, в которой преобразованы параметры структурных типов, рассматриваемое преобразование также заменяет вызовы целевых процедур, помеченные директивой #pragma spf transform replace(...) with(...)
на вызовы процедур, указанных в спецификации with
. При этом правила для выполнения замены должны быть указаны с помощью специфицкации #pragma spf metadata replace(...)
, добавляемой в тело процедуры, которая указана в спецификации with
.
Если внутри преобразуемой процедуры, содержащей заменяемые вызовы, заданы директивы replace
, содержащие не пустое множество параметров, то перед выполнением замены вызовов процедур будет создана копия процедуры, преобразованная в соответсвии с заданными директивами replace
. Затем внутри копии исходной процедуры будет выполнена замена вызовов процедур, помеченных с помощью спецификаций with
. В этом случае исходная процедура не будет подвергаться преобразованию. Если в теле исходной процедуры все директивы replace
содержат пустое множество параметров, то преобразована будет исходная процедуры и ее копия не будет создана.
Замечание. Допускается замена только части вызовов, для которых выполнены услоия допустимости преобразования, в частности найдены соответствующие спецификации #pragma spf metadata replace
.
Замечание. Присутствие директивы replace
, содержащей хотя бы один параметр, запрещает преобразование исходной процедуры и указывает на необходимость создания ее копии. Если при этом ни один из параметров, указанных в директиве, не может быть преобразован, то копия исходной процедуры создана не будет, и директивы with
будут игнорироваться.
Пример преобразуемой и преобразованной процедуры приведен в листингах 4.3 и 4.4 соответственно.
#define SIZE 4
typedef float DataTy;
struct STy {
DataTy T[SIZE];
};
void foo(struct STy *S, DataTy Val) { S->T[0] = Val; }
/* Replacement for void foo(struct STy *S) */
void foo_spf0(DataTy *S_T0, DataTy Val) {
#pragma spf metadata replace(foo({.T = S_T0}, Val))
S_T0[0] = Val;
}
void bar(struct STy *S, DataTy Val) {
#pragma spf transform replace(S) with(foo_spf0) nostrict
foo(S, Val);
}
В теле функции bar
задана спецификация replace
, указывающая на необходимость замены параметра S
, типом которого является указатель на структурный тип данных. Для функции foo
ранее уже была выполнена замена ее параметров, являющихся структурными типами данных. Соответствующая функция foo_spf0
, созданная не предшествующих шагах преобразования или добавленная в код вручную, содержит отображение своих параметров на параметры функции foo
.
Для задания отображения используется спецификация metadata replace(...)
. Внутри спецификации replace
указывается имя процедуры, вызов которой может быть заменен на вызов процедуры, содержащей спецификацию metadata
(в примере на листинге 4.3 указана функция foo
). После имени процедуры перечисляются правила для отображения всех параметров указанной процедуры. В данном случае указаны отображения для первого и второго параметров функции foo
.
Отображение для параметров структурных типов данных, обращения к которым должны быть разложены на обращения к отдельным элементам структур, указываются внутри блока {...}
: сначала указывается элемент структурного типа данных, затем параметр процедуры (внури которой расположена спецификация metadata
), который соответствует данному элементу структуры.
В примере, приведенном на листинге 4.3, первый параметр функции foo
был разложен на отдельные элементы внутри функции foo_spf0
. Так как внутри функции foo_spf0
, заменяющей функцию foo
используется только один элемент T
структуры STy
, то правило для замены первого параметра функции foo
имеет вид {.T = S_T0}
. Второй параметр функции foo
не является структурным типом данных и должен быть передан в функцию foo_spf0
как есть, а именно, в виде параметра Val
функции foo_spf0
Замечание. Параметры процедуры, используемой для замены, могут иметь типы указателей на элементы структуры. В этом случае соответствующие операции разыменования или взятия адреса будут добавлены в преобразованную процедуру автоматически. Описание отображения параметров с помощью replace metadata
при этом не изменяется. Так в листинге 4.3
в качестве первого параметра функции foo_spf0
мог быть использован параметр типа DataTy **
. При этом используемые директивы #pragma spf transform replace(...) with(...)
и #pragma spf metadata replace(...)
остались бы неизменными.
void bar(struct STy *S, DataTy Val) { foo(S, Val); }
/* Replacement for void bar(struct STy *S, DataTy Val) */
void bar_spf0(DataTy S_T1[4], DataTy Val) {
#pragma spf metadata replace(bar({.T = S_T1}, Val))
foo_spf0(S_T1, Val);
}
В результате преобразования исходная функция bar
останется неизменной за исключением удаления спецификаций, описывающих преобразование. Будет создана новая функция bar_spf0
, которая принимает в качестве первого параметра единственный элемент структуры STy
. Вызов функции foo
будет заменен на вызов функции foo_spf0
.
Написать нам можно через форму связи на сайте проекта DVM-системы.