FluentIL – Parte 2 – If..Else

Publicado em 09/02/2011

11


Olá pessoal, tudo certo?

No post anterior iniciamos a construção de um projeto open-source que objetiva  facilitar escrita de código para Emitting. O projeto se constitu, basicamente, de uma DSL que permite a escrita de código menos verboso e em nível mais alto (se comparado com a biblioteca de Emitting do .NET Framework).

Hoje, continuamos esse projeto adicionando alguns facilitadores para emissão de desvios condicionais. Se você não “entende” como funcionam desvios condicionais em Intermediate Language (que é a “linguagem” utilizada nos processos de emitting), considere a leitura da parte 4 de minha série sobre IL.

Geração de código executável on-the-fly, usando emitting, pode, em alguns cenários, ser uma alternativa atraente para gerar código com desempenho e flexibilidade incomparáveis.

Como estamos falando de três assuntos “densos” (IL, Emitting e DSLs), o código e o post acabam ficando, consequentemente, “densos” também. Em caso de dúvidas, me contate, ok!?

Lembre-se que este é um projeto open-source. Considere pegar o código fonte desse projeto em https://github.com/ElemarJR/FluentIL.

Desvios condicionais em IL usando “If..Goto”

Implementar desvios condicionais em IL não é exatamente uma operação trivial (assim como ocorre com loops). Isso ocorre porque IL não tem suporte nativo a comparações “If..Else..Endif”, mas sim “If..Goto”.

Considere o seguinte código em C#:

1 public static string Dummy(int a, int b) 2 { 3 if (a > b) 4 return "Yes"; 5 else 6 return "No"; 7 }

Agora, observe o mesmo código escrito em IL (já utilizando uma versão “aumentada” de nosso pequeno “toolkit”).

1 [Test] 2 public void TwoNumbersAreEquals_TrueReturnsYesFalseReturnsNo_Reference() 3 { 4 var dm = IL.NewMethod(typeof(string), typeof(int), typeof(int)) 5 .Ldarg(0, 1) 6 .Beq("if_true") 7 .Ldstr("No") 8 .Br("done") 9 .MarkLabel("if_true") 10 .Ldstr("Yes") 11 .MarkLabel("done") 12 .Ret(); 13 14 dm.Invoke(2, 2).Should().Be("Yes"); 15 dm.Invoke(2, 3).Should().Be("No"); 16 }

Ou ainda…

1 [Test] 2 public void TwoNumbersAreEquals_TrueReturnsYesFalseReturnsNo_Reference2() 3 { 4 var dm = IL.NewMethod(typeof(string), typeof(int), typeof(int)) 5 .Ldarg(0, 1) 6 .Ceq() 7 .Brfalse("if_false") 8 .Ldstr("Yes") 9 .Br("done") 10 .MarkLabel("if_false") 11 .Ldstr("No") 12 .MarkLabel("done") 13 .Ret(); 14 15 dm.Invoke(2, 2).Should().Be("Yes"); 16 dm.Invoke(2, 3).Should().Be("No"); 17 }

Repare que, mesmo utilizando uma interface fluente, o programador precisa estar familiriarizado com o conceito de “If .. Goto” (sentindo saudades do BASIC?!), bem menos intuitivo que o “If..Else..Endif” do C#.

Planejando um “If..Else..EndIf” da DSL de Emitting

Se IL não tem suporte nativo para “If..Else..EndIf”, implementamos o nosso!  Visando facilitar as coisas, incrementei a nossa DSL com algumas novas instruções (If..Else..Endif). Observe como a mesma comparação fica mais fácil de escrever:

1 [Test] 2 public void TwoNumbersAreEquals_TrueReturnsYesFalseReturnsNo() 3 { 4 var dm = IL.NewMethod(typeof(string), typeof(int), typeof(int)) 5 .Ldarg(0, 1) 6 .Ifeq() 7 .Ldstr("Yes") 8 .Else() 9 .Ldstr("No") 10 .EndIf() 11 .Ret(); 12 13 dm.Invoke(2, 2).Should().Be("Yes"); 14 dm.Invoke(2, 3).Should().Be("No"); 15 }

 

Bem melhor, não acha? Sem labels, sem desvios.

Alegre

Implementando o “suporte” a desvios condicionais “If..Goto” em nossa DSL

Como indiquei na parte 4 de minha série sobre IL, Intermediate Language oferece instruções distintas para cada tipo de comparação. Manter a “familiaridade” com Intermediate Language implicar na implementação de diversos métodos muito parecidos onde o que mudaria seria apenas o OpCode sendo “emitido”. Por isso, visando diminuir erros, recorri a um pouco de T4 para essa atividade. Observe:

1 public partial class DynamicMethodBody 2 { 3 <# string [] opcodes = new string [] { 4 "Beq", "Beq_S", 5 "Bne_Un", "Bne_Un_S", 6 "Bge", "Bge_S", "Bge_Un", "Bge_Un_S", 7 "Bgt", "Bgt_S", "Bgt_Un", "Bgt_Un_S", 8 "Ble", "Ble_S", "Ble_Un", "Ble_Un_S", 9 "Blt", "Blt_S", "Blt_Un", "Blt_Un_S", 10 "Brtrue", "Brfalse", 11 "Br", "Br_S" }; 12 foreach (var opcode in opcodes) 13 { 14 #> 15 #region <#= opcode #> 16 public DynamicMethodBody <#= opcode #>(string label) 17 { 18 return <#= opcode #>(GetLabel(label)); 19 } 20 21 public DynamicMethodBody <#= opcode #>(Label label) 22 { 23 return Emit(OpCodes.<#= opcode #>, label); 24 } 25 #endregion 26 }

Perceba que:

  • tornei DynamicMethodBody uma classe parcial, permitindo que eu escrevesse um arquivo T4 (extensão .tt) para automatizar a geração dos diversos métodos;
  • utilizei um array de strings com o nome de todas as instruções IL suportadas pela biblioteca de emitting;
  • para cada Opcode gero duas sobrecargas de método. Uma recebendo o label da posição destino (desvio) e outra recebendo uma string (aproveitando o “simplificador de uso” de labels apresentado no post anterior).

Se você ainda não sabe T4, trate de aprender (código mecânico [quase idêntico, com pequenas modificações], para mim, ferem o conceito DRY. São débitos técnicos, aceite-os, mas pague-os em seus projetos!).

Implementando o “suporte” a desvios condicionais “If..Else..EndIf” em nossa DSL

Em IL, cada instrução opera de forma intependente das outras. Sua relação é sempre com o contexto (valores na pilha e no Heap). Instruções “If..Else..Endif” não seguem esse princípio, são dependentes.

Implemento o suporte a “If..Else..EndIf” partindo do segundo exemplo desse post para “If..Goto”. Para operacionalizar isso em nossa DSL criei uma nova entidade: IfEmitter. Observe:

1 class IfEmitter 2 { 3 readonly DynamicMethodBody _Generator; 4 readonly string _IfFalse; 5 readonly string _Done; 6 bool _WithElse; 7 8 public IfEmitter(DynamicMethodBody generator) 9 { 10 _Generator = generator; 11 _IfFalse = string.Format("IfFalse_{0}", Guid.NewGuid()); 12 _Done = string.Format("IfFalse_{0}", Guid.NewGuid()); 13 } 14 15 public void EmitIf(OpCode comparasionOpcode) 16 { 17 _Generator 18 .Emit(comparasionOpcode) 19 .Brfalse(_IfFalse); 20 } 21 22 public void EmitElse() 23 { 24 _Generator 25 .Br(_Done) 26 .MarkLabel(_IfFalse); 27 28 _WithElse = true; 29 } 30 31 public void EmitEndIf() 32 { 33 if (!_WithElse) _Generator.MarkLabel(_IfFalse); 34 _Generator.MarkLabel(_Done); 35 } 36 }

Essa entidade mantem e controla algumas condições de estado importantes para implementação do “If..Else..Endif”. Como pode perceber, mantenho identificadores para o desvio para condição “falso” e para “pronto”. Além disso, condiciono o emit do EndIf a presença do Else.

A implementação dos “comandos” em nossa DSL foi feita assim:

1 public partial class DynamicMethodBody 2 { 3 readonly Stack<IfEmitter> _IfEmitters = new Stack<IfEmitter>(); 4 public DynamicMethodBody Else() 5 { 6 _IfEmitters.Peek().EmitElse(); 7 return this; 8 } 9 10 public DynamicMethodBody EndIf() 11 { 12 _IfEmitters.Pop().EmitEndIf(); 13 return this; 14 } 15 16 <# string [] opcodes = new string [] { 17 "Beq", "Beq_S", 18 "Bne_Un", "Bne_Un_S", 19 "Bge", "Bge_S", "Bge_Un", "Bge_Un_S", 20 "Bgt", "Bgt_S", "Bgt_Un", "Bgt_Un_S", 21 "Ble", "Ble_S", "Ble_Un", "Ble_Un_S", 22 "Blt", "Blt_S", "Blt_Un", "Blt_Un_S", 23 "Brtrue", "Brfalse", 24 "Br", "Br_S" }; 25 foreach (var opcode in opcodes) 26 { 27 #> 28 #region <#= opcode #> 29 <# 30 if (!opcode.StartsWith("Br") && 31 !opcode.StartsWith("Bge") && !opcode.EndsWith("_S") 32 && !opcode.Equals("Ble") && !opcode.Equals("Ble_Un") 33 && !opcode.Equals("Bne_Un") 34 ) { #> 35 public DynamicMethodBody C<#= opcode.Substring(1) #>() 36 { 37 return Emit(OpCodes.C<#= opcode.Substring(1) #>); 38 } 39 40 public DynamicMethodBody If<#= opcode.Substring(1) #>() 41 { 42 var emitter = new IfEmitter(this); 43 _IfEmitters.Push(emitter); 44 emitter.EmitIf(OpCodes.C<#= opcode.Substring(1) #>); 45 return this; 46 } 47 <# }#> 48 #endregion 49 50 <# 51 } 52 #> 53 }

Perceba que esse fragmento de T4 foi extraído do mesmo código usado para gerar o suporte para “If..Goto”. A geração dos diversos “comandos” IfXXX está condicionada a disponibilidade de instruções de comparação simples. Observe também o empilhamento de “IfEmitters” para permitir a utilização de If’s aninhados

Por hoje, era isso.

Smiley piscando

No próximo post, implementamos o Switch (o que acham?!)

Publicado em: Emitting