FluentIL – Parte 7 – Expressions, atributos, Log, whatever!

Publicado em 13/05/2011

0


Olá pessoal, como estamos?

Há alguns dias não escrevia nada aqui no blog. Quero começar a falar sobre compiladores mas não estou conseguindo acertar o post inicial.

Hoje vou relatar alguns avanços realizados na FluentIL. Se você não sabe do que se trata, consulte os posts anteriores dessa série.

Lembre-se, todo código-fonte está disponível em https://github.com/elemarjr/FluentIL.

Suporte a Expressions

Quando falei sobre “tradução” de expressions, mostrei a implementação de um Visitor que percorre um determinado tipo de expressions fazendo chamadas para a FluentIL. Essa alternativa simplifica bastante o emitting de alguns códigos complexos.

Além do Visitor que apresentei, escrevi outro que simplifica expressões. Considere o seguinte T4:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

<#
	var types = new [] {"Add", "Subtract", "Multiply", "Divide", 
		"GreaterThanOrEqual", "LessThanOrEqual", "GreaterThan", "LessThan"};
	var operators = new [] {"+", "-", "*", "/", ">=", "<=", ">", "<"};
#>

namespace FluentIL.ExpressionInterpreter
{
    public class ExpressionSimplifierVisitor :
        ExpressionVisitor
    {
        protected override Expression VisitBinary(BinaryExpression node)
        {
<#for (int i = 0; i < types.Length; i++) { #>
            if (node.NodeType == ExpressionType.<#=types[i]#>)
            {
                var left = Visit(node.Left);
                var right = Visit(node.Right);

                if (left is ConstantExpression && right is ConstantExpression)
                {
                    if (left.Type == typeof(int) && right.Type == typeof(int))
                    {
                        int lvalue = (int)((ConstantExpression)left).Value;
                        int rvalue = (int)((ConstantExpression)right).Value;
                        return Expression.Constant(lvalue <#= operators[i] #> rvalue);
                    }
                    else if (left.Type == typeof(double) && right.Type == typeof(double))
                    {
                        double lvalue = (double)((ConstantExpression)left).Value;
                        double rvalue = (double)((ConstantExpression)right).Value;
                        return Expression.Constant(lvalue <#= operators[i] #> rvalue);
                    }
                    else if (left.Type == typeof(float)  && right.Type == typeof(float))
                    {
                        double lvalue = (float)((ConstantExpression)left).Value;
                        double rvalue = (float)((ConstantExpression)right).Value;
                        return Expression.Constant(lvalue <#= operators[i] #> rvalue);
                    }
                }
            }
<#}#>
            return base.VisitBinary(node);
        }
    }
}

A lógica desse código é simples. Estou substituindo todas as operações binárias que envolvam constantes por novas constantes resultantes dessas expressões. Assim, o emitting de um “(2 + ( 2 / 2 ))” fica sendo “3". Ou seja, otimização óbvia, mas boa.

Para permitir a inclusão de Expressions na interface fluente, disponibilizo um Helper method  novo em DynamicMethodBody. Observe:

public DynamicMethodBody Expression(Expression expression)
{
    expression = new ExpressionSimplifierVisitor().Visit(expression);
    new ILEmitterVisitor(this).Visit(
        expression
        );
    return this;
}

Basicamente, submeto a expression que foi fornecida para o Visitor que apresentei acima. Ou seja, faço toda simplificação óbvia que for possível. Depois disso, submeto a expression resultante ao Visitor que efetiva o emitting. Fino! Observe um teste escrito considerando essa possibilidade (já apresentado no post anterior):

[Test]
public void ConditionOr()
{
    var method = IL.NewMethod()
        .WithParameter(typeof(int), "a")
        .Returns(typeof(bool))
        .Expression(
            Expression.OrElse(
                Expression.AndAlso(
                    Expression.GreaterThan(
                            Expression.Parameter(typeof(int), "a"),
                            Expression.Constant(5)
                            ),
                    Expression.LessThan(
                            Expression.Parameter(typeof(int), "a"),
                            Expression.Constant(10)
                            )
                    )
                ,
                Expression.AndAlso(
                    Expression.GreaterThan(
                            Expression.Parameter(typeof(int), "a"),
                            Expression.Constant(15)
                            ),
                    Expression.LessThan(
                            Expression.Parameter(typeof(int), "a"),
                            Expression.Constant(20)
                            )
                    )
                )
            )
        .Ret();

    method.Invoke(5).Should().Be(false);
    method.Invoke(7).Should().Be(true);
    method.Invoke(10).Should().Be(false);
    method.Invoke(11).Should().Be(false);
    method.Invoke(17).Should().Be(true);
}

Conciliar Emitting com Expressions pode facilitar brutalmente a escrita mais “complexos”, como o indicado acima. Segue o IL gerado por FluentIL nos bastidores:

	ldarg.0
	ldc.i4.5
	cgt
	brfalse IL_0
	ldarg.0
	ldc.i4 10
	clt
	br.s IL_1
IL_0:
	ldc.i4.0
IL_1:
	brtrue IL_2
	ldarg.0
	ldc.i4 15
	cgt
	brfalse IL_3
	ldarg.0
	ldc.i4 20
	clt
	br.s IL_4
IL_3:
	ldc.i4.0
IL_4:
	br.s IL_5
IL_2:
	ldc.i4.1
IL_5:
	ret

Bacana, não!? Instrumentei todo o código da FluentIL para gerar uma saída do código IL-like que está sendo gerado. Assim, posso ter uma idéia da eficiência da linguagem.

Como você deve lembrar, criei uma abstração (estratégia) para representar parâmetros numéricos: a classe Number. Abaixo, segue minha implementação de Number para Expressions:

public class ExpressionNumber : Number
{
    public Expression Expression { get; private set; }
    public ExpressionNumber(Expression expression)
    {
        this.Expression = expression;
    }

    public override void Emit(DynamicMethodBody generator)
    {
        generator.Expression(this.Expression);
    }
   
}

Adoro estratégias. Agora, consigo passar uma Expression por parâmetro em qualquer método da DSL que espere um número.

Instrumentando a DSL

Como disse acima, gosto muito da idéia de saber o código que FluentIL está gerando. Para isso, modifiquei alguns métodos chave e todas as sobrecargas do método Emit para  gerar uma saída em Debug. Observe algumas dessas alterações:

public DynamicMethodBody Emit(OpCode opcode, double arg)
{
	ExecutePreEmitActions();
	#if DEBUG
	Debug.WriteLine("\t{0} {1}", opcode, arg.ToString());
				#endif
			
	_Info.GetILGenerator()
        .Emit(opcode, arg);

    return this;
}

public DynamicMethodBody Emit(OpCode opcode, Label arg)
{
	ExecutePreEmitActions();
	#if DEBUG
	Debug.WriteLine("\t{0} IL_{1}", opcode, arg.GetHashCode());
				#endif
			
	_Info.GetILGenerator()
        .Emit(opcode, arg);

    return this;
}

public DynamicMethodBody Emit(OpCode opcode, MethodInfo arg)
{
	ExecutePreEmitActions();
	#if DEBUG
	Debug.WriteLine("\t{0} {1}", opcode, arg.ToString());
				#endif
			
	_Info.GetILGenerator()
        .Emit(opcode, arg);

    return this;
}

public DynamicMethodBody Emit(OpCode opcode, ConstructorInfo arg)
{
	ExecutePreEmitActions();
	#if DEBUG
	Debug.WriteLine("\t{0} {1}", opcode, arg.ToString());
				#endif
			
	_Info.GetILGenerator()
        .Emit(opcode, arg);

    return this;
}

public DynamicMethodBody Emit(OpCode opcode, FieldInfo arg)
{
	ExecutePreEmitActions();
	#if DEBUG
	Debug.WriteLine("\t{0} {1}", opcode, arg.Name);
				#endif
			
	_Info.GetILGenerator()
        .Emit(opcode, arg);

    return this;
}

Agora, ao executar os testes no NUnit, posso consultar o IL gerado em cada cenário.

image

Adicionando suporte a atributos de instância (instance fields)

Queria fazer o emitting de uma classe assim:

public class Counter
{
    int currentValue = 0;

    public void Increment()
    {
        this.currentValue++;
    }

    public void Decrement()
    {
        this.currentValue--;
    }

    public int GetCurrentValue()
    {
        return this.currentValue;
    }
}

Entretanto, há um problema! FluentIL não tinha suporte para emitting de atributos. Como resolvi? Adicionei um método Helper WithField na classe DynamicTypeInfo. Observe:

List<DynamicFieldInfo> _fields = new List<DynamicFieldInfo>();
public DynamicTypeInfo WithField(string fieldName, Type fieldType)
{
    var value = new DynamicFieldInfo(
        this,
        fieldName, 
        fieldType);

#if DEBUG
    Debug.Print(".field ({0}) {1}", fieldType, fieldName);
#endif

    _fields.Add(
        value
        );

    if (this.TypeBuilderField != null)
    {
        value.FieldBuilder = this.TypeBuilderField.DefineField(
                    fieldName,
                    fieldType,
                    FieldAttributes.Private
                    );
    }
    return this;
}

O que faço? Mantenho uma coleção de todos os “fields” que foram adicionados para, posteriormente, fazer a definição no TypeBuilder caso ele ainda não esteja definido.

Agora, vejamos a implementação dos métodos da classe gerada pelo compilador do C#:

.method public hidebysig instance void  Decrement() cil managed
{
  // Code size       16 (0x10)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  dup
  IL_0003:  ldfld      int32 ConsoleApplication3.Counter::currentValue
  IL_0008:  ldc.i4.1
  IL_0009:  sub
  IL_000a:  stfld      int32 ConsoleApplication3.Counter::currentValue
  IL_000f:  ret
} // end of method Counter::Decrement

.method public hidebysig instance void  Increment() cil managed
{
  // Code size       16 (0x10)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  dup
  IL_0003:  ldfld      int32 ConsoleApplication3.Counter::currentValue
  IL_0008:  ldc.i4.1
  IL_0009:  add
  IL_000a:  stfld      int32 ConsoleApplication3.Counter::currentValue
  IL_000f:  ret
} // end of method Counter::Increment

.method public hidebysig instance int32  GetCurrentValue() cil managed
{
  // Code size       12 (0xc)
  .maxstack  1
  .locals init ([0] int32 CS$1$0000)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldfld      int32 ConsoleApplication3.Counter::currentValue
  IL_0007:  stloc.0
  IL_0008:  br.s       IL_000a
  IL_000a:  ldloc.0
  IL_000b:  ret
} // end of method Counter::GetCurrentValue

E também nossa transposição para direta para FluentIL:

[Test]
public void Counter_BasicILVersion()
{
    var cti = IL.NewType()
        .Implements<ICounter>()
        .WithField("currentValue", typeof(int));

    var field = cti.GetFieldInfo("currentValue");

    cti
        .WithMethod("Increment")
        .Returns(typeof(void))
            .Emit(OpCodes.Nop)
            .Ldarg(0)
            .Dup()
            .Emit(OpCodes.Ldfld, field)
            .Ldc(1)
            .Add()
            .Emit(OpCodes.Stfld, field)
            .Ret()
        .WithMethod("Decrement")
        .Returns(typeof(void))
            .Emit(OpCodes.Nop)
            .Ldarg(0)
            .Dup()
            .Emit(OpCodes.Ldfld, field)
            .Ldc(1)
            .Sub()
            .Emit(OpCodes.Stfld, field)
            .Ret()
        .WithMethod("GetCurrentValue")
        .WithVariable(typeof(int))
        .Returns(typeof(int))
            .Emit(OpCodes.Nop)
            .Ldarg(0)
            .Emit(OpCodes.Ldfld, field)
            .Stloc(0)
            .Br_S("IL_000a")
            .MarkLabel("IL_000a")
            .Ldloc(0)
            .Ret();

    var counter = (ICounter)Activator.CreateInstance(cti.AsType);

    counter.GetCurrentValue().Should().Be(0);
    counter.Increment();
    counter.GetCurrentValue().Should().Be(1);
    counter.Decrement();
    counter.GetCurrentValue().Should().Be(0);
}

Muito do código gerado pelo compilador é questionável… mas esse é assunto para outro post.

O principal contra da DSL nesse caso foi, além da carência de alguns métodos helper, a necessidade de obter a instância do field antes dos emittings. Feitas modificações óbvias e algumas ampliações chegamos em:

[Test]
public void Counter_Simplifying()
{
    var cti = IL.NewType()
        .Implements<ICounter>()
        .WithField("currentValue", typeof(int))
        .WithMethod("Increment")
        .Returns(typeof(void))
            .Ldarg(0)
            .Dup()
            .Ldfld("currentValue")
            .Add(1)
            .Stfld("currentValue")
            .Ret()
        .WithMethod("Decrement")
        .Returns(typeof(void))
            .Ldarg(0)
            .Dup()
            .Ldfld("currentValue")
            .Sub(1)
            .Stfld("currentValue")
            .Ret()
        .WithMethod("GetCurrentValue")
        .Returns(typeof(int))
            .Ldarg(0)
            .Ldfld("currentValue")
            .Ret();

    var counter = (ICounter)Activator.CreateInstance(cti.AsType);

    counter.GetCurrentValue().Should().Be(0);
    counter.Increment();
    counter.GetCurrentValue().Should().Be(1);
    counter.Decrement();
    counter.GetCurrentValue().Should().Be(0);
}

O que eu fiz? Retirei todo código inútil gerado pelo compilador do C#. Além disso, criei “sobrecargas” espertas de Ldfld e Stfld que recuperam informação do atributo diretamente na info do tipo que está sendo construído. Nada muito avançado, mas demasiado longo para um post. Lembre-se, todo código-fonte está disponível em https://github.com/elemarjr/FluentIL.

Por hoje, era isso.

Smiley piscando

Publicado em: Emitting, Post