Olá pessoal, como estamos?
Esse é o terceiro dia consecutivo que escrevo nessa série. Estou muito contente com os avanços obtidos até aqui nessa DSL.
No post de hoje, escrevo outro código trivial em C# e, partindo do IL gerado pelo compilador, faço adaptações na DSL para melhorar a fluência. Nesse processo, identifico uma oportunidade de melhoria brutal no código já escrito partindo da aplicação do Strategy pattern;
O código-fonte está disponível em https://github.com/elemarjr/FluentIL
Vamos aos fatos.
Código de referência
Antes de qualquer coisa, vamos ao nosso código de referência de hoje. Observe:
public bool IsPrime(int number)
{
if (number < 2) return false;
for (int i = 2; i <= number / 2; i++)
if ((number % i) == 0) return false;
return true;
}
O código é muito simples. Recebendo um número inteiro, verifico se este é primo. Para isso, percorro todos os números de 2 até metade daquele que está sendo verificado.
Lembrando que é considerado primo todo número positivo, maior igual a 2, que seja divisível apenas por 1 e por ele mesmo.
Agora, vejamos a versão em IL escrita pelo compilador:
.method public hidebysig instance bool IsPrime(int32 number) cil managed
{
// Code size 29 (0x1d)
.maxstack 3
.locals init ([0] int32 i)
IL_0000: ldarg.1
IL_0001: ldc.i4.1
IL_0002: bgt.s IL_0006
IL_0004: ldc.i4.0
IL_0005: ret
IL_0006: ldc.i4.2
IL_0007: stloc.0
IL_0008: br.s IL_0015
IL_000a: ldarg.1
IL_000b: ldloc.0
IL_000c: rem
IL_000d: brtrue.s IL_0011
IL_000f: ldc.i4.0
IL_0010: ret
IL_0011: ldloc.0
IL_0012: ldc.i4.1
IL_0013: add
IL_0014: stloc.0
IL_0015: ldloc.0
IL_0016: ldarg.1
IL_0017: ldc.i4.2
IL_0018: div
IL_0019: ble.s IL_000a
IL_001b: ldc.i4.1
IL_001c: ret
} // end of method Program::IsPrime
Se você não sabe IL, recomendo a leitura da série introdutória que escrevi sobre o assunto.
Intermediate Language não fornece recursos muito facilitados para execução de loops. Todo loop que escrevemos em C# (como o For de nosso exemplo de hoje é convertido em uma sequência de Jumps).
Simplificando o uso (e teste) do código que iremos gerar com emitting
No parte 4, apresentei uma ampliação significativa na DSL. Com ela, é possível gerar tipos inteiramente novos usando o mecanismo (antes era possível gerar apenas métodos).
Hoje, utilizo o artifício de criar um tipo implementando uma interface. A interface que desenvolvi para o código acima foi:
public interface IPrimeChecker
{
bool IsPrime(int number);
}
Mantenho o princípio de que devo confirmar a implementação com testes. Para hoje, escrevi testes bem básicos que testam o retorno do método para números primos e não-primos. Observe:
[Test]
public void IsPrimeV1_Passing0_ReturnsFalse()
{
var checker = CreatePrimeCheckerV1();
checker.IsPrime(0).Should().Be(false);
}
[Test]
public void IsPrimeV1_Passing1_ReturnsFalse()
{
var checker = CreatePrimeCheckerV1();
checker.IsPrime(1).Should().Be(false);
}
[Test]
public void IsPrimeV1_Passing2_Returnstrue()
{
var checker = CreatePrimeCheckerV1();
checker.IsPrime(2).Should().Be(true);
}
[Test]
public void IsPrimeV1_Passing3_ReturnsTrue()
{
var checker = CreatePrimeCheckerV1();
checker.IsPrime(3).Should().Be(true);
}
[Test]
public void IsPrimeV1_Passing4_ReturnsFalse()
{
var checker = CreatePrimeCheckerV1();
checker.IsPrime(4).Should().Be(false);
}
Mais uma vez, extraí o método que realiza o emitting de cada teste.
Primeira versão com FluentIL (transcrevendo, apenas!)
Para começar, resolvi escrever apenas uma transposição simples do código gerado pelo compilador para FluentIL. Observe o código resultante:
public IPrimeChecker CreatePrimeCheckerV1()
{
var t = IL.NewType().Implements<IPrimeChecker>()
.WithMethod("IsPrime")
.WithVariable(typeof(int), "i")
.WithParameter(typeof(int), "number")
.Returns(typeof(bool))
.Ldarg("number") // IL_0000: ldarg.1
.Ldc(1) // IL_0001: ldc.i4.1
.Bgt_S("IL_0006") // IL_0002: bgt.s IL_0006
.Ldc(0) // IL_0004: ldc.i4.0
.Ret() // IL_0005: ret
.MarkLabel("IL_0006").Ldc(2) // IL_0006: ldc.i4.2
.Stloc("i") // IL_0007: stloc.0
.Br_S("IL_0015") // IL_0008: br.s IL_0015
.MarkLabel("IL_000a").Ldarg("number") // IL_000a: ldarg.1
.Ldloc("i") // IL_000b: ldloc.0
.Emit(OpCodes.Rem) // IL_000c: rem
.Brtrue_S("IL_0011")// IL_000d: brtrue.s IL_0011
.Ldc(0) // IL_000f: ldc.i4.0
.Ret() // IL_0010: ret
.MarkLabel("IL_0011").Ldloc("i") // IL_0011: ldloc.0
.Ldc(1) // IL_0012: ldc.i4.1
.Add() // IL_0013: add
.Stloc("i") // IL_0014: stloc.0
.MarkLabel("IL_0015").Ldloc("i") // IL_0015: ldloc.0
.Ldarg("number") // IL_0016: ldarg.1
.Ldc(2) // IL_0017: ldc.i4.2
.Div() // IL_0018: div
.Ble("IL_000a") // IL_0019: ble.s IL_000a
.Ldc(1) // IL_001b: ldc.i4.1
.Ret() // IL_001c: ret
.AsType;
return (IPrimeChecker)Activator.CreateInstance(t);
}
Diferente do que ocorreu no exemplo de ontem, praticamente todas as intruções geradas pelo compilador encontram correspondência na interface fluente da DSL que estamos desenvolvendo (apenas a instrução Rem [que retorna o resto] não possuia correspondência).
Repare como, mesmo nessa simples transposição, há facilidades percebíveis. Próximo passo: reduzir código usando benefícios de nossa DSL.
Segunda versão com FluentIL (aplicando recursos avançados da DSL)
Vimos que a transposição simples de IL para FluentIL pode ser realizada sem maiores dificuldades. Agora, vamos revisitar o código fazendo substituições óbvias.
Entretanto, antes de avançar, incluí em DynamicMethodBody um “helper” para o OpCode Rem e uma sobrecarga para o helper Ret que aceita um boolean. Observe:
public DynamicMethodBody Ret(bool returnValue)
{
return this
.Ldc(returnValue ? 1 : 0)
.Ret();
}
A nova versão para IsPrime com as modificações possibilitadas ganha bastante em legibilidade. Observe:
public IPrimeChecker CreatePrimeCheckerV2()
{
var t = IL.NewType().Implements<IPrimeChecker>()
.WithMethod("IsPrime")
.WithVariable(typeof(int), "i")
.WithParameter(typeof(int), "number")
.Returns(typeof(bool))
.Ldarg("number")
.IfNotgt(1)
.Ret(false)
.EndIf()
.Stloc(2, "i") // for
.Br_S("IL_0015") // for
.MarkLabel("IL_000a") // for
.Ldarg("number") // number % i
.Ldloc("i") // number % i
.Rem() // number % i
.Brtrue_S("IL_0011") // if
.Ret(false)
.MarkLabel("IL_0011") // endif
.AddToVar("i", 1) // for
.MarkLabel("IL_0015").Ldloc("i") // for
.Ldarg("number") // for
.Div(2) // for
.Ble("IL_000a") // for
.Ret(true)
.AsType;
return (IPrimeChecker)Activator.CreateInstance(t);
}
Observe como a utilização de sobregargas “mais inteligentes” de nossos helpers deixam o código menos verboso (destaque para o método de retorno). Entretanto, observe que não foi possível retirar a complexidade envolvida com o loop For. Isso ocorreu pois nosso helper for trabalhava apenas com (números) constantes. Graças ao nosso exemplo de hoje, isso vai mudar!
Aplicando Strategy pattern para adicionar flexibilidade
Antes da modificação que apresento agora, ao examinar o código dos Helpers contidos em DynamicMethodBody era possível encontrar um grande número de sobrecargas de helpers para diferentes tipos de parâmetros. Observe uma pequena amostra:
public DynamicMethodBody EnsureLimits(int min, int max)
{
return this
.Dup()
.LdcI4(min)
.Iflt()
.Pop()
.LdcI4(min)
.Else()
.Dup()
.LdcI4(max)
.Ifgt()
.Pop()
.LdcI4(max)
.EndIf()
.EndIf();
}
public DynamicMethodBody EnsureLimits(double min, double max)
{
return this
.Dup()
.LdcR8(min)
.Iflt()
.Pop()
.LdcR8(min)
.Else()
.Dup()
.LdcR8(max)
.Ifgt()
.Pop()
.LdcR8(max)
.EndIf()
.EndIf();
}
Perceba que esses dois métodos são praticamente iguais. Aqui temos um “débito técnico” em sua essência. Mas qual a solução? Strategy, evidentemente! Ou seja, separar o comportamento diferente em implementações distintas. Para isso, crio uma “classe base” que isole o conceito e crio especializações para cada estratégia. Observe:
public abstract class Number
{
public abstract void Emit(DynamicMethodBody generator);
public static implicit operator Number(int value)
{
return new ConstantInt32Number(value);
}
public static implicit operator Number(double value)
{
return new ConstantDoubleNumber(value);
}
public static implicit operator Number(string varName)
{
return new VarNumber(varName);
}
}
public class ConstantInt32Number : Number
{
public int Value { get; private set; }
public ConstantInt32Number(int value)
{
this.Value = value;
}
public override void Emit(DynamicMethodBody generator)
{
generator.Ldc(this.Value);
}
}
public class ConstantDoubleNumber : Number
{
public double Value { get; private set; }
public ConstantDoubleNumber(double value)
{
this.Value = value;
}
public override void Emit(DynamicMethodBody generator)
{
generator.Ldc(this.Value);
}
}
public class VarNumber : Number
{
public string VarName { get; private set; }
public VarNumber(string varName)
{
this.VarName = varName;
}
public override void Emit(DynamicMethodBody generator)
{
if (generator.GetVariableIndex(this.VarName) > -1)
generator.Ldloc(this.VarName);
else
generator.Ldarg(this.VarName);
}
}
Lindo, não é mesmo?! Utilizei um “implicit converter” para converter números para nossas versões com estratégia (inteiros para ConstantIntNumber, double para ConstantDoubleNumber e, por fim, string para VarNumber).
VarNumber suporta uma nova funcionalidade: a utilização de variáveis locais ou parâmetros como argumentos em nossos helpers que trabalham com números.
Do lado do DynamicMethodBody, implemento uma sobrecarga para o Helper Emit. Basicamente, ela ativa a estratégia. Obseve:
public DynamicMethodBody Emit(params Number[] numbers)
{
foreach (var number in numbers)
number.Emit(this);
return this;
}
Agora, nossa versão atualizada de EnsureLimits ficou assim:
public DynamicMethodBody EnsureLimits(Number min, Number max)
{
return this
.Dup()
.Emit(min)
.Iflt()
.Pop()
.Emit(min)
.Else()
.Dup()
.Emit(max)
.Ifgt()
.Pop()
.Emit(max)
.EndIf()
.EndIf();
}
Adeus repetições, adeus lógica duplicada! Graças ao Strategy pattern, adeus débito técnico.
Observe as novas implementações para os helpers For e Next:
public DynamicMethodBody For(string variable, Number from, Number to, int step = 1)
{
var ilgen = this._Info.GetILGenerator();
var beginLabel = ilgen.DefineLabel();
var comparasionLabel = ilgen.DefineLabel();
_Fors.Push(new ForInfo(variable, from, to, step,
beginLabel, comparasionLabel));
if (GetVariableIndex(variable) == -1)
{
this._Info.WithVariable(typeof(int), variable);
ilgen.DeclareLocal(typeof(int));
}
this
.Emit(from)
.Stloc(variable)
.Br(comparasionLabel)
.MarkLabel(beginLabel);
return this;
}
public DynamicMethodBody Next()
{
var f = _Fors.Pop();
this
.Ldloc(f.Variable)
.Ldc(f.Step)
.Add()
.Stloc(f.Variable)
.MarkLabel(f.ComparasionLabel)
.Ldloc(f.Variable)
.Emit(f.To);
if (f.Step > 0)
this.Ble(f.BeginLabel);
else
this.Bge(f.BeginLabel);
return this;
}
Saiba que ForInfo é um objeto “depósito” que faz a “ligação” entre o Helper For e Next. Veja como a utilização de Number nos argumentos habilita a utilização desses helpers tanto com constantes quanto com variáveis/argumentos.
Terceira versão com FluentIL (aplicando For)
O refactoring que fizemos em nosso código abre espaço para mais uma simplicação no nosso case de hoje. Observe:
public IPrimeChecker CreatePrimeCheckerV3()
{
var t = IL.NewType().Implements<IPrimeChecker>()
.WithMethod("IsPrime")
.WithVariable(typeof(int), "i")
.WithVariable(typeof(int), "half")
.WithParameter(typeof(int), "number")
.Returns(typeof(bool))
.Ldarg("number")
.IfNotgt(1)
.Ret(false)
.EndIf()
.Ldarg("number")
.Div(2)
.Stloc("half")
.For("i", 2, "half")
.Ldarg("number")
.Rem("i")
.Ifeq(0)
.Ret(false)
.EndIf()
.Next()
.Ret(true)
.AsType;
return (IPrimeChecker)Activator.CreateInstance(t);
}
Lindo! Nosso emitting gera um IL mais otimizado que o compilador do C# já que fazemos a divisão apenas uma vez. Repare como a lógica de nosso código em FluentIL se aproximou bastante daquela que escrevemos em C#.
Por hoje, era isso.
Lembre-se, o código-fonte está disponível em https://github.com/elemarjr/FluentIL
![]()






junho 1st, 2011 → 1:36
[...] fim, também criei uma especialização da classe Number (parte 6) para permitir que expressões sejam passadas por parâmetro para métodos que esperam números. [...]