Статьи

Метапрограммирование во время компиляции в Groovy, часть 1

Я начал играть с преобразованиями AST в Groovy несколько дней назад ( обзор главы о преобразованиях AST (GINA2) ). Будучи настолько взволнован этой технологией, я решил написать несколько постов (пошаговых руководств), описывающих, как добавить метапрограммирование времени компиляции в ваш повседневный арсенал.

В этом посте я собираюсь продемонстрировать, как написать локальное преобразование AST. Преобразования, которые я собираюсь написать, немного упрощены, но они действительно работают, и не будет проблемой добавить больше функциональности поверх них.

Быть убежденным в том, что код говорит лучше, чем слова, — это простой тест Спока:

class ExecuteOnceTransformationTest extends Specification{

    def 'should execute a method only once'() {
        when:
        def testObject = new GroovyShell().evaluate('''
            class TestClass {
                int counter = 0

                @victorsavkin.sample1.ExecuteOnce
                void executeOnceMethod(){
                    counter ++
                }
            }
            new TestClass()
        ''')

        then:
        testObject.counter == 0

        when:
        testObject.executeOnceMethod()

        then:
        testObject.counter == 1

        when:
        testObject.executeOnceMethod()

        then:
        testObject.counter == 1
    }

    def 'should generate a compilation error if return type is not void'() {
        when:
        new GroovyShell().evaluate('''
            class TestClass {
                @victorsavkin.sample1.ExecuteOnce
                def executeOnceMethod(){
                    'ReturnValue'
                }
            }
            new TestClass()
        ''')

        then:
        def e = thrown(MultipleCompilationErrorsException)
        e.errorCollector.errorCount == 1
        def firstError = e.errorCollector.errors.first()
        firstError.cause.message.startsWith('ExecuteOnce can be applied only to methods returning void') == true
    }
}

Как вы можете видеть, преобразование, которое я собираюсь написать, может быть применено к методам, и оно изменит AST, чтобы гарантировать, что метод будет выполнен только один раз. Также это преобразование может быть применено только к методам, возвращающим void. Пример из реальной жизни, где его можно использовать: вы можете аннотировать ваш метод init с помощью аннотации @ExecuteOnce, а затем вызывать его во всех ваших методах. Инициализация будет выполнена только один раз. Когда наш тест готов, мы можем приступить к реализации всех частей.

Создать аннотацию

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(['victorsavkin.sample1.ExecuteOnceTransformation'])
@interface ExecuteOnce {
}

Поскольку нам нужно только представить нашу аннотацию во время компиляции, мы используем политику хранения SOURCE. Нетрудно понять, что @GroovyASTTransformationClass определяет класс, который будет использоваться для выполнения нашего преобразования.

Создать класс, реализующий ASTTransformation

@GroovyASTTransformation(phase = SEMANTIC_ANALYSIS)
class ExecuteOnceTransformation implements ASTTransformation {

    ...

    void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        ...
    }
}

SEMANTIC_ANALYSIS — это самая ранняя фаза, которую можно использовать для локального преобразования. Чем раньше ваш этап, тем меньше деталей реализации у вас есть в вашем AST. Так что если вы хотите манипулировать Groovy-кодом, вы должны использовать ранние фазы. Если вы хотите манипулировать сгенерированным кодом Java (включая сгенерированные методы получения и установки), возможно, лучшим выбором будет INSTRUCTION_SELECTION.

ASTTransformation — очень запутанный интерфейс, так как он используется как локальными, так и глобальными преобразованиями и используется по-разному. В этом примере мы будем использовать astNodes для манипулирования AST и sourceUnit для добавления ошибок компиляции.

Создайте объект спецификации для проверки объекта astNodes.

class ExecuteOnceTransformationSpecification {
    boolean isRightReturnType(astNodes){...}
    SyntaxException getSyntaxExceptionForInvalidReturnType(astNodes){...}
    boolean shouldSkipTransformation(astNodes){...}
}

Чтобы сделать этот пост короче, я не включил код ExecuteOnceTransformationSpecification. Вы можете найти весь исходный код в репозитории github, который я создал для этого поста.

Создать AstFactory для создания частей АСТ

Создание фрагментов AST выглядит для меня немного как черная магия, поэтому я думаю, что было бы неплохо изолировать его внутри этой фабрики.

class ExecuteOnceAstFactory {

def createGuardIfStatement(fieldName) {
def ast = new AstBuilder().buildFromString SEMANTIC_ANALYSIS, true, """
if(! ${fieldName}){
${fieldName} = true
}
"""
ast[0].statements[0]
}

def generatePrivateFieldNode(fieldName) {
def ast = new AstBuilder().buildFromString SEMANTIC_ANALYSIS, false, """
class ${fieldName}_Class {
private boolean ${fieldName} = false
}
"""
ast[1].fields.find{it.name == fieldName}
}
}

Как вы можете видеть, я использую AstBuilder.buildFromString для создания фрагментов AST. Есть много способов создать AST, но buildFromString для меня пока выглядит самым простым. Я использую одну и ту же фазу компиляции SEMANTIC_ANALYSIS для обеих частей. Не волнуйтесь, если не поняли, что делает этот кусок кода. Я бы посоветовал вам запускать тесты с помощью отладчика и просто посмотреть, что будет генерироваться в каждом случае. После нескольких попыток понимание придет к вам.

Соберите все части вместе, чтобы закончить преобразование

@GroovyASTTransformation(phase = SEMANTIC_ANALYSIS)
class ExecuteOnceTransformation implements ASTTransformation {

    private specification = new ExecuteOnceTransformationSpecification()
    private astFactory = new ExecuteOnceAstFactory()

    void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        if(specification.shouldSkipTransformation(astNodes))
            return

        if(!specification.isRightReturnType(astNodes)){
            sourceUnit.addError specification.getSyntaxExceptionForInvalidReturnType(astNodes)
            return
        }

        MethodNode method = astNodes[1]
        def fieldName = createFieldName(method.name)

        addGuardFieldToClass fieldName, method
        addGuardIfStatementToMethod fieldName, method
    }

    private addGuardFieldToClass(fieldName, method) {
        def field = astFactory.generatePrivateFieldNode(fieldName)
        method.declaringClass.addField field
    }

    private addGuardIfStatementToMethod(fieldName, method) {
        def guardStatement = astFactory.createGuardIfStatement(fieldName)
        addAllStatementsOfMethodIntoGuardIf(guardStatement, method)
        method.code = guardStatement
    }

    private addAllStatementsOfMethodIntoGuardIf(guardIfStatement, method) {
        BlockStatement ifBlock = guardIfStatement.ifBlock
        ifBlock.statements.addAll(method.code.statements)
    }

    private createFieldName(methodName) {
        '$_victorsavkin_samples_execute_once_guard_for_' + methodName
    }
}

Возвращаясь к нашему примеру, если у вас есть этот класс:

class TestClass {
int counter = 0

@victorsavkin.sample1.ExecuteOnce
void executeOnceMethod(){
counter ++
}
}

Скомпилированный код будет выглядеть так:

class TestClass {
private $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod = false;
int counter = 0

void executeOnceMethod(){
if(! $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod){
$_victorsavkin_samples_execute_once_guard_for_executeOnceMethod = true;
counter ++
}
}
}

Это так просто. В реальной жизни мы должны решать проблемы параллелизма, но это усложнит пример, поэтому я не делал этого здесь.

Подводить итоги:

  1. Написать собственное преобразование AST — не самая простая вещь, но она выполнима и, что также очень важно, тестируема. Кроме того, он позволяет вам изменить семантику языка, что невозможно сделать с помощью метапрограммирования во время выполнения.
  2. Я бы посоветовал вам купить Groovy в действии 2, если вы хотите узнать больше об этой теме. Я бы не стал ждать выхода финальной версии и купить ее прямо сейчас.
  3. Если вы хотите получить удовольствие от метапрограммирования во время компиляции, взгляните на Спока. Это хорошая иллюстрация того, что можно сделать с помощью преобразований AST и нельзя сделать с помощью метапрограммирования во время выполнения.

GitHub Repository

https://github.com/avix1000/AST-Transformations

 

С http://victorsavkin.com/post/4568615925/compile-time-metaprogramming-in-groovy-part-1