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.
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.
![]()






Publicado em 13/05/2011
0