Вы находитесь на странице: 1из 26

Cree un compilador de lenguaje para .

NET Framework
Contenido
Los piratas informticos son grandes celebridades dentro del mundo de la informtica. He visto cmo Anders Hejlsberg haca una presentacin en la Conferencia de Desarrolladores Profesionales y, a continuacin, bajaba del escenario para dirigirse hacia una multitud de gente que le pedan autgrafos y sacarle fotos. Existe cierta mstica intelectual acerca de la gente que se dedica a aprender y comprender los intrngulis de las expresiones lambda, los sistemas de tipos y los lenguajes ensambladores. Bien, pues ahora tambin es posible compartir parte de esta gloria escribiendo su propio compilador para Microsoft .NET Framework. Existen centenares de compiladores para docenas de lenguajes destinados a .NET Framework. CLR de .NET pone todos estos lenguajes en el mismo saco y los combina de tal manera que interactan ente s a la perfeccin. Un desarrollador ninja puede beneficiarse de esto al crear sistemas de software grandes: agregando un poco de C# y una pizca de Python. Por supuesto, estos desarrolladores son impresionantes, pero no tienen comparacin con los verdaderos maestros, los piratas informticos de compiladores. Son stos ltimos los que conocen verdaderamente los equipos virtuales, el diseo del lenguaje y la esencia de los compiladores. En este artculo veremos el cdigo para un compilador escrito en C# (tambin conocido como compilador "Good for Nothing") y, a medida que vayamos avanzando, nos iremos introduciendo en la arquitectura de alto nivel, las teoras y en las API de .NET Framework que son necesarias para crear nuestro propio compilador .NET. Empezaremos con una definicin de lenguaje, exploraremos la arquitectura del compilador y, a continuacin, veremos el subsistema de generacin de cdigo que se obtiene de un ensamblado .NET. El objetivo es entender las bases para el desarrollo de los compiladores y comprender clara y perfectamente cmo los lenguajes estn programados para CLR de forma eficaz. No voy a desarrollar el equivalente de C# 4.0 ni de IronRuby, pero an as esta explicacin ser lo suficientemente interesante como para encender su pasin por el arte del desarrollo de compiladores.

Definicin de lenguaje
Los lenguajes de software empiezan con un propsito determinado. Este propsito puede ir desde la necesidad de obtener mayor expresividad (como Visual Basic), productividad (como Python, que tiene como objetivo obtener el mximo rendimiento de cada lnea de cdigo), especializacin (como Verilog, que es un lenguaje de descripcin de hardware que usan los fabricantes de procesadores) o simplemente la necesidad de satisfacer las preferencias personales del creador. (Al creador de Boo, por ejemplo, le gusta .NET Framework pero no le gusta ninguno de los lenguajes disponibles en ste). Una vez determinado nuestro propsito, ya podemos disear el lenguaje; imaginmoslo como una hoja de ruta. Los lenguajes informticos deben ser muy precisos, para que el programador pueda expresar con exactitud lo que es necesario y el compilador pueda entenderlo con precisin y as generar cdigo ejecutable para lo que se ha expresado exactamente. La hoja de ruta de un lenguaje debe especificarse para poder quitar ambigedades durante la implementacin del compilador.

Para hacerlo, se usa una metasintaxis. Se trata de una sintaxis que se emplea para describir la sintaxis de lenguajes. Existen bastantes metasintaxis, as que podemos elegir la que ms nos guste. Aqu voy a usar el lenguaje Good for Nothing junto con una metasintaxis denominada EBNF (Extended Backus-Naur Form). Es importante mencionar que los orgenes de EBNF son de confianza: se la vincula a John Backus, el ganador del Premio Turing y responsable de desarrollado de FORTRAN. Una explicacin ms profunda de EBNF se escapa del tema de este artculo, pero explicar sus conceptos ms bsicos. La definicin de lenguaje para Good for Nothing se muestra en la figura 1. De acuerdo con mi definicin de lenguaje, Statement (stmt) puede ser declaraciones variables, asignaciones, bucles For, lectura de enteros desde la lnea de comandos o impresiones para la pantalla; y todos pueden especificarse muchas veces, delimitados por puntos y comas. Expressions (expr) pueden ser cadenas, enteros, expresiones aritmticas o identificadores. Identifiers (ident) pueden denominarse por medio de un carcter alfabtico como, por ejemplo, la primera letra seguida de caracteres o nmeros. Y as sucesivamente. Para expresarlo de forma sencilla, hemos definido una sintaxis de lenguaje que proporciona capacidades aritmticas bsicas, un sistema de tipos pequeo y una interaccin con el usuario sencilla y basada en la consola. Figure 1 Definicin del lenguaje Good for Nothing
<stmt> := var <ident> = <expr> | <ident> = <expr> | for <ident> = <expr> to <expr> do <stmt> end | read_int <ident> | print <expr> | <stmt> ; <stmt>

<expr> := <string> | <int> | <arith_expr> | <ident>

<arith_expr> := <expr> <arith_op> <expr> <arith_op> := + | - | * | /

<ident> := <char> <ident_rest>* <ident_rest> := <char> | <digit>

<int> := <digit>+ <digit> := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

<string> := " <string_elem>* " <string_elem> := <any char other than ">

Quizs haya notado que esta definicin de lenguaje es breve en lo que refiere a especificidades. No he especificado el nmero (por ejemplo, si puede ser mayor de un entero de 32 bits) y si siquiera si puede ser un nmero negativo. Una verdadera definicin de EBNF precisamente definira todos estos detalles pero, en aras de la concisin, nos ceiremos a este ejemplo sencillo. A continuacin, tenemos una muestra del programa del lenguaje Good for Nothing:
var ntimes = 0; print "How much do you love this company? (1-10) "; read_int ntimes; var x = 0; for x = 0 to ntimes do print "Developers!"; end; print "Who said sit down?!!!!!";

A fin de poder entender mejor el funcionamiento de la gramtica, podemos comparar este programa sencillo con la definicin de lenguaje. Y con esto, ya tenemos lista la definicin de lenguaje.

Arquitectura de alto nivel


Uno de los trabajos del compilador es traducir tareas de alto nivel creadas mediante el programador en tareas que un procesador pueda entender y ejecutar. Es decir, el compilador toma un programa escrito en el lenguaje Good for Nothing y lo traduce a algo que CLR de .NET puede ejecutar. Un compilador realiza esto a travs de una serie de pasos de traduccin, dividiendo el lenguaje en las partes que nos interesan y eliminando el resto. Los compiladores siguen los principios habituales de diseo de software: componentes de acoplamiento flexible, denominados fases, y conectados entre s para ejecutar juntos cada paso de la traduccin. La figura 2 ilustra los componentes que llevan a cabo las fases de un compilador: el detector, el analizador y el generador de cdigo. En cada fase, el lenguaje se divide ms y ms, y la informacin acerca del objetivo del programa sirve para la fase siguiente.

Figura 2 Las fases del compilador (Hacer clic en la imagen para ampliarla)
Los tipos de compiladores a menudo agrupan abstractamente las fases en front end y back end. El tipo front end consiste en la deteccin y el anlisis, mientras que el back end consiste normalmente en la generacin de cdigo. El trabajo del front end es detectar la estructura sintctica de un programa y convertirla desde texto a una representacin en memoria de alto nivel denominada rbol de sintaxis abstracta (AST), algo que elaboraremos dentro de un momento. El back end tiene la tarea de tomar el AST y convertirlo en algo que un equipo pueda ejecutar. Las tres fases se dividen generalmente en front end y back end porque detectores y analizadores normalmente se emparejan juntos, mientras que, generalmente, el generador de cdigo se empareja ms con una plataforma de destino. Este diseo permite que, en caso de que el lenguaje deba ser una plataforma cruzada, los desarrolladores puedan reemplazar el generador de cdigo para diferentes plataformas. El cdigo del compilador Good for Nothing est disponible en la descarga que acompaa a este artculo. Iremos viendo los componentes de cada fase y explorando los detalles de su implementacin.

El detector
El trabajo principal del detector es dividir texto (una secuencia de caracteres en el archivo de cdigo fuente) en fragmentos (denominados muestras) que el analizador pueda consumir. El detector determina qu muestras deben, al final, enviarse al analizador y, por lo tanto, puede desechar los elementos que no se definen en la gramtica como, por ejemplo, los comentarios. En cuanto al lenguaje Good for Nothing, el detector se centra en caracteres (A-Z y los smbolos habituales), nmeros (0-9), caracteres que definen las operaciones (tal como +, -, * y /), comillas para la encapsulacin de cadenas, y puntos y comas. Un detector agrupa secuencias de caracteres relacionados entre s en muestras que pasarn al analizador. Por ejemplo, la secuencia de caracteres " h e l l o w o r l d !" se agrupara en una muestra: "hello world!". El detector Good for Nothing es sorprendentemente sencillo y requiere slo de un System.IO.TextReader en las instancias. Esto inicia el proceso de deteccin, tal como se muestra a continuacin:
public Scanner(TextReader input) { this.result = new Collections.List<object>(); this.Scan(input); }

La figura 3 ilustra el mtodo Scan, el cual tiene un sencillo bucle While que repasa cada carcter de la secuencia de texto y busca caracteres reconocibles declarados en la definicin de lenguaje. Cada vez que encuentra un carcter o fragmento de caracteres reconocible, el detector crea una muestra y la agrega a una List<object>. (En este caso, lo escribo como objeto. Sin embargo, podra haber creado una clase Token u otra clase parecida para encapsular ms informacin acerca de la muestra como, por ejemplo, los nmeros de lnea y de columna). Figure 3 El mtodo de deteccin del detector
private void Scan(TextReader input) {

while (input.Peek() != -1) { char ch = (char)input.Peek();

// Scan individual tokens if (char.IsWhiteSpace(ch)) { // eat the current char and skip ahead input.Read(); } else if (char.IsLetter(ch) || ch == '_') { StringBuilder accum = new StringBuilder();

input.Read(); // skip the '"'

if (input.Peek() == -1) { throw new Exception("unterminated string literal"); }

while ((ch = (char)input.Peek()) != '"') { accum.Append(ch); input.Read();

if (input.Peek() == -1) { throw new Exception("unterminated string literal"); } }

// skip the terminating "

input.Read(); this.result.Add(accum); }

... } }

Como puede ver, cuando el cdigo encuentra un carcter ", se asume que encapsular una muestra de cadena; por lo tanto, consumo la cadena, la envuelvo en una sesin StringBuilder y la agrego a la lista. Una vez que el detector ha creado la lista de muestras, stas pasan a la clase del analizador a travs de una propiedad denominada Tokens.

El analizador
El analizador es el ncleo del compilador y adopta muchas formas y tamaos. El analizador Good for Nothing tiene varios trabajos: se asegura de que el programa de origen cumpla con la definicin de lenguaje y administra los resultados de errores, en caso de haberlos. Crea tambin la representacin en memoria de la sintaxis del programa, la cual consume el generador de cdigo, y, finalmente, el analizador Good for Nothing decide qu tipos de tiempo de ejecucin deben usarse. La primera cosa que hay que hacer es ver la representacin en memoria de la sintaxis del programa, el AST. Luego, veremos el cdigo que crea este rbol a partir de las muestras del detector. El formato AST es rpido, eficiente, fcil de codificar y puede recorrerse muchas veces por el generador de cdigo. Lafigura 4 muestra este AST para el compilador Good for Nothing. Figure 4 AST para el compilador Good for Nothing
public abstract class Stmt { }

// var <ident> = <expr> public class DeclareVar : Stmt { public string Ident; public Expr Expr; }

// print <expr> public class Print : Stmt {

public Expr Expr; }

// <ident> = <expr> public class Assign : Stmt { public string Ident; public Expr Expr; }

// for <ident> = <expr> to <expr> do <stmt> end public class ForLoop : Stmt { public string Ident; public Expr From; public Expr To; public Stmt Body; }

// read_int <ident> public class ReadInt : Stmt { public string Ident; }

// <stmt> ; <stmt> public class Sequence : Stmt { public Stmt First; public Stmt Second; }

/* <expr> := <string>

* * * */

| <int> | <arith_expr> | <ident>

public abstract class Expr { }

// <string> := " <string_elem>* " public class StringLiteral : Expr { public string Value; }

// <int> := <digit>+ public class IntLiteral : Expr { public int Value; }

// <ident> := <char> <ident_rest>* // <ident_rest> := <char> | <digit> public class Variable : Expr { public string Ident; }

// <arith_expr> := <expr> <arith_op> <expr> public class ArithExpr : Expr { public Expr Left; public Expr Right; public BinOp Op;

// <arith_op> := + | - | * | / public enum ArithOp { Add, Sub, Mul, Div }

Un vistazo rpido a la definicin Good for Nothing nos muestra que AST coincide ligeramente con los nodos de definicin de lenguaje a partir de la gramtica EBNF. Es mejor pensar en la definicin de lenguaje como un modo de encapsular la sintaxis, mientras que el rbol de sintaxis abstracto captura la estructura de estos elementos. Hay muchos algoritmos disponibles para el anlisis, pero examinarlos todos no forma parte del objetivo de este artculo. En general, stos difieren en la forma de revisar la secuencia de muestras para crear el AST. En nuestro compilador Good for Nothing, hemos usado lo que se llama un analizador de arriba a abajo de LL (derivacin de izquierda a derecha, al mximo a la izquierda). Esto significa simplemente que lee texto de izquierda a derecha y construye el AST basado en la siguiente muestra de entrada disponible. El constructor para mi clase de analizador toma simplemente una lista de muestras creadas por el detector:
public Parser(IList<object> tokens) { this.tokens = tokens; this.index = 0; this.result = this.ParseStmt();

if (this.index != this.tokens.Count) throw new Exception("expected EOF"); }

El trabajo esencial de anlisis lo realiza el mtodo ParseStmt, tal como se muestra en la figura 5. Devuelve un nodo Stmt, que sirve como el nodo raz del rbol y coincide con el nodo superior de definicin de la sintaxis del lenguaje. El analizador recorre la lista de muestras que usan un ndice para marcar la posicin actual al tiempo que identifican muestras subordinadas al nodo Stmt en la sintaxis del lenguaje (declaraciones y asignaciones variables, bucles For, read_ints e impresiones). Si una muestra no puede identificarse, se genera una excepcin.

Figure 5 El mtodo ParseStmt identifica muestras


private Stmt ParseStmt() { Stmt result;

if (this.index == this.tokens.Count) { throw new Exception("expected statement, got EOF"); }

if (this.tokens[this.index].Equals("print")) { this.index++; ... } else if (this.tokens[this.index].Equals("var")) { this.index++; ... } else if (this.tokens[this.index].Equals("read_int")) { this.index++; ... } else if (this.tokens[this.index].Equals("for")) { this.index++; ... } else if (this.tokens[this.index] is string) { this.index++;

... } else { throw new Exception("parse error at token " + this.index + ": " + this.tokens[this.index]); } ... }

Cuando se identifica una muestra, se crea un nodo AST y no es necesario que el nodo realice ms anlisis. A continuacin, se muestra el cdigo que se requiere para crear el nodo AST de impresin:
// <stmt> := print <expr> if (this.tokens[this.index].Equals("print")) { this.index++; Print print = new Print(); print.Expr = this.ParseExpr(); result = print; }

En este punto suceden dos cosas. La muestra de impresin se descarta aumentando el contador de ndices y se realiza una llamada al mtodo ParseExpr para obtener un nodo Expr, ya que la definicin del lenguaje requiere que la muestra de impresin vaya seguida de una expresin. La figura 6 muestra el cdigo ParseExpr. Recorre la lista de muestras a partir del ndice actual e identifica muestras que satisfacen la definicin de lenguaje de una expresin. En este caso, el mtodo simplemente busca cadenas, nmeros enteros y variables (creados por la sesin del escner) y devuelve los nodos AST apropiados que representan estas expresiones. Figure 6 ParseExpr realiza el anlisis de nodos de expresin
// <expr> := <string> // | <int> // | <ident> private Expr ParseExpr() { ... if (this.tokens[this.index] is StringBuilder) {

string value = ((StringBuilder)this.tokens[this.index++]).ToString(); StringLiteral stringLiteral = new StringLiteral(); stringLiteral.Value = value; return stringLiteral; } else if (this.tokens[this.index] is int) { int intValue = (int)this.tokens[this.index++]; IntLiteral intLiteral = new IntLiteral(); intLiteral.Value = intValue; return intLiteral; } else if (this.tokens[this.index] is string) { string ident = (string)this.tokens[this.index++]; Variable var = new Variable(); var.Ident = ident; return var; } ... }

Para obtener instrucciones de cadena que cumplan con la definicin de sintaxis del lenguaje <"stmt> <; stmt>", se usa el nodo de la secuencia AST. Este nodo de secuencia contiene dos punteros para los nodos stmt y forma la base de la estructura del rbol AST. La informacin siguiente muestra el cdigo que se us para tratar con este caso de secuencias:
if (this.index < this.tokens.Count && this.tokens[this.index] == Scanner.Semi) { this.index++;

if (this.index < this.tokens.Count && !this.tokens[this.index].Equals("end")) {

Sequence sequence = new Sequence(); sequence.First = result; sequence.Second = this.ParseStmt(); result = sequence; } }

El rbol AST que se muestra en la figura 7 es el resultado del siguiente fragmento de cdigo Good for Nothing:

Figura 7 helloworld.gfn AST Tree y High-Level Trace (Hacer clic en la imagen para ampliarla)
var x = "hello world!"; print x;

Para .NET Framework


Antes de adentrarnos en el cdigo procedente de la generacin de cdigo, debo retroceder un poco y hablar de mi objetivo. Por tanto, ahora pasar a describir los servicios del compilador que CLR de .NET ofrece, incluidos el equipo virtual basado en la pila, el sistema de tipos y las bibliotecas que se usan para crear el ensamblado .NET. Tambin hablar brevemente de las herramientas que se requieren para identificar y diagnosticar los errores de los resultados del compilador. CLR es un equipo virtual, lo que significa que es un software que emula un sistema informtico. Como cualquier equipo, CLR dispone de un conjunto de operaciones de bajo nivel que puede realizar, un conjunto de servicios en memoria y un lenguaje ensamblador para definir los programas ejecutables. Un CLR usa una estructura de datos abstracta basada en la pila para modelar la ejecucin de cdigo, as como un lenguaje ensamblador llamado Idioma Intermedio (IL) para definir aquellas operaciones que pueden realizarse en la pila. Cuando se ejecuta un programa definido en IL, CLR simula simplemente las operaciones especificadas en una pila, insertando e incluyendo datos para que se ejecutan mediante una instruccin. Suponga que desea agregar dos nmeros mediante IL. ste es el cdigo que se usa para realizar 10 + 20:

ldc.i4 ldc.i4 add

10 20

La primera lnea (ldc.i4 10) inserta el nmero entero 10 en la pila. Luego, la segunda lnea (ldc.i4 20) inserta el nmero entero 20 en la pila. La tercera lnea (add) excluye los dos nmeros enteros de la pila, los agrega y, finalmente, inserta el resultado en la pila. La simulacin del equipo de la pila tiene lugar al traducir IL y la semntica de la pila en el lenguaje del procesador del equipo subyacente, ya sea durante el tiempo de ejecucin a travs de la compilacin justo a tiempo (JIT) o bien previamente a travs de servicios como el Generador de imgenes nativas (Ngen). Existen muchas instrucciones IL disponibles para crear programas y van desde la aritmtica bsica y el control de flujo hasta una variedad de convenciones de llamada. En el documento Partition III de la especificacin de la Asociacin Europea de Fabricantes de Equipos (ECMA) (disponible enmsdn2.microsoft.com/aa569283), puede encontrarse informacin relativa a todas las instrucciones IL. La pila abstracta de CLR realiza las operaciones sobre otros elementos aparte de los enteros. Dispone de un sistema de tipos enriquecido que incluye cadenas, enteros, booleanos, floats, doubles, etctera. Para que mi lenguaje pueda ejecutarse sin problemas en el CLR e interoperar con otros lenguajes compatibles con .NET, incorporar parte del sistema de tipos de CLR a mi propio programa. Concretamente, el lenguaje Good for Nothing define dos tipos, nmeros y cadenas, y los asigno a System.Int32 y System.String. El compilador Good for Nothing usa un componente de la biblioteca de clases base (BCL) denominado System.Reflection.Emit para tratar con la generacin de cdigo IL, y la creacin y empaquetado de ensamblado .NET. Es una biblioteca de bajo nivel que se cie al hardware de tal forma que proporciona abstracciones sencillas de generacin de cdigo en lenguaje IL. La biblioteca se usa tambin en otras API de BCL muy conocidas, incluida System.Xml.XmlSerializer. Las clases de alto nivel que se requieren para crear un ensamblado .NET (tal como se muestra en lafigura 8) siguen de alguna manera el patrn de diseo de software del constructor con una API de constructor para cada abstraccin lgica de metadatos .NET. La clase AssemblyBuilder se usa para crear el archivo PE y configurar los elementos necesarios de metadatos del ensamblado .NET como el manifiesto. La clase ModuleBuilder se usa para crear mdulos dentro del ensamblado. TypeBuilder se usa para crear Types y sus metadatos asociados. MethodBuilder y LocalBuilder administran la adicin de mtodos a tipos y locales a mtodos respectivamente. La clase ILGenerator se usa para generar el cdigo IL para mtodos usando la clase OpCodes, que es una enumeracin larga que contiene todas las posibles instrucciones IL. Todas estas clases Reflection.Emit se usan en el generador de cdigo Good for Nothing.

Figura 8 Las bibliotecas Reflection.Emit usadas para crear un ensamblado .NET (Hacer clic en la imagen para ampliarla)

Herramientas para obtener derecho a IL


Incluso la mayora de piratas informticos de compiladores con experiencia cometen errores en el nivel de generacin cdigo. El error ms comn es el cdigo incorrecto de IL, el cual causa desequilibrios en la pila. Normalmente, CLR genera una excepcin cuando encuentra un IL incorrecto (ya sea al cargar el ensamblado o bien cuando IL se compila justo a tiempo, dependiendo del nivel de confianza del ensamblado). Diagnosticar y reparar estos errores es una tarea sencilla si se realiza con una herramienta SDK llamada peverify.exe. Esta herramienta lleva a cabo la comprobacin de IL y se asegura de que el cdigo es correcto y puede ejecutarse de forma segura. Por ejemplo, aqu estn los cdigos IL que pretende agregar el nmero 10 a la cadena "incorrecta":
ldc.i4 ldstr add 10 "bad"

Al ejecutar peverify en un ensamblado que contiene este IL incorrecto, se obtendr como resultado el siguiente error:
[IL]: Error: [C:\MSDNMagazine\Sample.exe : Sample::Main][offset 0x0000002][found ref 'System .String'] Expected numeric type on the stack.

En este ejemplo, peverify notifica que la instruccin de agregar esperaba dos tipos numricos y en su lugar encontr un entero y una cadena. ILASM (ensamblador IL) y ILDASM (desensamblador IL) son dos herramientas SDK que pueden usarse para compilar texto IL en ensamblados .NET y descompilar ensamblados para IL respectivamente. ILASM permite probar rpida y fcilmente las secuencias de instrucciones IL que sern la base del resultado del compilador. Slo hay que crear el cdigo IL de prueba en un editor

de texto y transmitirlo a ILASM. Mientras tanto, la herramienta de ILDASM puede inspeccionar rpidamente en el IL que se haya generado un compilador para una ruta de acceso de cdigo determinada. Esto incluye el IL que los compiladores comerciales emiten como, por ejemplo, el compilador de C#. Proporciona una buena manera de consultar el cdigo IL para instrucciones que son similares entre lenguajes; es decir, el cdigo del control de flujo de IL generado para un bucle For de C# podra volver a usarse por otros compiladores con constructores similares.

El generador de cdigo
El generador de cdigo para el compilador Good for Nothing depende en gran manera de la biblioteca Reflection.Emit para poder producir un ensamblado .NET ejecutable. Pasar ahora a describir y analizar las partes importantes de esta clase, y el resto podr examinarlo con ms detenimiento en su tiempo libre. El constructor CodeGen, que aparece en la figura 9, configura la infraestructura Reflection.Emit, que es la que a su vez se requiere antes de empezar a emitir cdigo. Empiezo definiendo el nombre de ensamblado y pasndolo al constructor de ensamblado. En este ejemplo, voy a usar el nombre del archivo del cdigo fuente para el nombre del ensamblado. A continuacin, la definicin ModuleBuilder: para una definicin del mdulo, ste usa el mismo nombre que el ensamblado. A continuacin, definir un TypeBuilder en el ModuleBuilder que retiene el nico tipo del ensamblado. No existen dos tipos definidos de primera clase de la definicin de lenguaje Good for Nothing, pero al menos un tipo s es necesario para retener el mtodo, el cual se ejecutar en el inicio. MethodBuilder define un mtodo Main para retener el IL que se generar para el cdigo Good for Nothing. Tendr que llamar a SetEntryPoint en este MethodBuilder, de forma que se ejecute en el inicio cuando el usuario ejecute el archivo ejecutable. Finalmente, creo ILGenerator global (il) a partir de MethodBuilder mediante el mtodo GetILGenerator. Figure 9 El constructor CodeGen
Emit.ILGenerator il = null; Collections.Dictionary<string, Emit.LocalBuilder> symbolTable;

public CodeGen(Stmt stmt, string moduleName) { if (Path.GetFileName(moduleName) != moduleName) { throw new Exception("can only output into current directory!"); }

AssemblyName name = new AssemblyName(Path.GetFileNameWithoutExtension(moduleName)); Emit.AssemblyBuilder asmb = AppDomain.CurrentDomain.DefineDynamicAssembly(name, Emit.AssemblyBuilderAccess.Save); Emit.ModuleBuilder modb = asmb.DefineDynamicModule(moduleName);

Emit.TypeBuilder typeBuilder = modb.DefineType("Foo");

Emit.MethodBuilder methb = typeBuilder.DefineMethod("Main", Reflect.MethodAttributes.Static, typeof(void), System.Type.EmptyTypes);

// CodeGenerator this.il = methb.GetILGenerator(); this.symbolTable = new Dictionary<string, Emit.LocalBuilder>();

// Go Compile this.GenStmt(stmt);

il.Emit(Emit.OpCodes.Ret); typeBuilder.CreateType(); modb.CreateGlobalFunctions(); asmb.SetEntryPoint(methb); asmb.Save(moduleName); this.symbolTable = null; this.il = null; }

Una vez configurada la infraestructura Reflection.Emit, el generador de cdigo llama al mtodo GenStmt, que, a su vez, se usa para recorrer el AST. Esto genera el cdigo IL necesario a travs de ILGenerator global. La figura 10 muestra un subconjunto del mtodo GenStmt, el cual, ante una primera llamada, empieza con un nodo Sequence y sigue hasta recorrer el AST y cambiar en el tipo de nodo AST actual. Figure 10 El subconjunto del mtodo GenStmt
private void GenStmt(Stmt stmt) { if (stmt is Sequence) { Sequence seq = (Sequence)stmt; this.GenStmt(seq.First);

this.GenStmt(seq.Second); }

else if (stmt is DeclareVar) { ... }

else if (stmt is Assign) { ... } else if (stmt is Print) { ... } }

A continuacin, se muestra el cdigo para el nodo DeclareVar (declaracin de variables) de AST:


else if (stmt is DeclareVar) { // declare a local DeclareVar declare = (DeclareVar)stmt; this.symbolTable[declare.Ident] = this.il.DeclareLocal(this.TypeOfExpr(declare.Expr));

// set the initial value Assign assign = new Assign(); assign.Ident = declare.Ident; assign.Expr = declare.Expr; this.GenStmt(assign); }

Llegados a este punto, lo primero que se necesita es agregar la variable a una tabla de smbolos. La tabla de smbolos es una estructura de datos elemental del compilador que se usa para asociar un identificador simblico (en este caso, el nombre de la variable basado en la cadena) con su tipo, ubicacin y mbito dentro de un programa. La tabla Good for Nothing de smbolos es sencilla, igual que todas las declaraciones variables son locales para el mtodo Main. Por lo tanto, asocio un smbolo con un LocalBuilder mediante un sencillo Dictionary<string, LocalBuilder>. Tras agregar el smbolo a la tabla de smbolos, traduzco el nodo DeclareVar de AST a un nodo Asign a fin de asignar la expresin de declaracin variable a la variable. Uso el siguiente cdigo para generar instrucciones de asignacin:
else if (stmt is Assign) { Assign assign = (Assign)stmt; this.GenExpr(assign.Expr, this.TypeOfExpr(assign.Expr)); this.Store(assign.Ident, this.TypeOfExpr(assign.Expr)); }

Al hacer esto, se genera el cdigo de IL para cargar una expresin en la pila y, a continuacin, emitir IL y almacenar la expresin en el LocalBuilder apropiado. El cdigo GenExpr que se muestra en la figura 11 toma un nodo Expr AST y emite el IL que se requiere para cargar la expresin en el equipo de la pila. StringLiteral y IntLiteral son similares en tanto que ambos tienen instrucciones IL directas que cargan las cadenas y los enteros respectivos en la pila: Ldstr y ldc.i4. Figure 11 El mtodo GenExpr
private void GenExpr(Expr expr, System.Type expectedType) { System.Type deliveredType;

if (expr is StringLiteral) { deliveredType = typeof(string); this.il.Emit(Emit.OpCodes.Ldstr, ((StringLiteral)expr).Value); } else if (expr is IntLiteral) { deliveredType = typeof(int); this.il.Emit(Emit.OpCodes.Ldc_I4, ((IntLiteral)expr).Value); } else if (expr is Variable) {

string ident = ((Variable)expr).Ident; deliveredType = this.TypeOfExpr(expr);

if (!this.symbolTable.ContainsKey(ident)) { throw new Exception("undeclared variable '" + ident + "'"); }

this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[ident]); } else { throw new Exception("don't know how to generate " + expr.GetType().Name); }

if (deliveredType != expectedType) { if (deliveredType == typeof(int) && expectedType == typeof(string)) { this.il.Emit(Emit.OpCodes.Box, typeof(int)); this.il.Emit(Emit.OpCodes.Callvirt, typeof(object).GetMethod("ToString")); } else { throw new Exception("can't coerce a " + deliveredType.Name + " to a " + expectedType.Name); } } }

Las expresiones variables cargan simplemente una variable local del mtodo en la pila mediante una llamada a ldloc y pasando el LocalBuilder respectivo. El ltimo apartado acerca del cdigo que se muestra en la figura 11 trata la conversin del tipo de expresin al tipo esperado (denominado coercin de tipos). Por ejemplo, un tipo puede necesitar una conversin en una llamada al mtodo de impresin en la que se requiere la conversin de un entero en una cadena para que la impresin pueda realizarse correctamente. La figura 12 muestra cmo las variables se asignan a expresiones en el mtodo Store. El nombre se busca a travs de la tabla de smbolos y, a continuacin, el LocalBuilder respectivo pasa a la instruccin stloc de IL. Esta accin incluye sencillamente la expresin actual de la pila y la asigna a la variable local. Figure 12 Almacenamiento de expresiones en una variable
private void Store(string name, Type type) { if (this.symbolTable.ContainsKey(name)) { Emit.LocalBuilder locb = this.symbolTable[name];

if (locb.LocalType == type) { this.il.Emit(Emit.OpCodes.Stloc, this.symbolTable[name]); } else { throw new Exception("'" + name + "' is of type " + locb.LocalType.Name + " but attempted to store value of type " + type.Name); } } else { throw new Exception("undeclared variable '" + name + "'"); } }

El cdigo que se usa para generar el IL para el nodo Print de AST es importante, ya que llama a un mtodo BCL. La expresin se genera en la pila y la instruccin de llamada de IL se usa para llamar al

mtodo System.Console.WriteLine. Reflection se usa para obtener el controlador del mtodo WriteLine, el cual se necesita para pasarlo a la instruccin de la llamada:
else if (stmt is Print) { this.GenExpr(((Print)stmt).Expr, typeof(string)); this.il.Emit(Emit.OpCodes.Call, typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(string) })); }

Cuando se realiza una llamada a un mtodo, los argumentos del mtodo se excluyen de la pila de tal modo que el ltimo que entra es el primero de la pila. Es decir, el primer argumento del mtodo es el elemento superior de la pila, el segundo argumento es el elemento siguiente, etc. El cdigo ms complejo es el cdigo que genera IL para los bucles For de Good for Nothing (consulte lafigura 13). Es bastante parecido a cmo los compiladores comerciales generaran esta clase de cdigo. Sin embargo, la mejor manera de explicar el cdigo del bucle For es ver el IL que se genera, el cual se muestra en la figura 14. Figure 14 Cdigo IL para el bucle For
// for x = 0 IL_0006: IL_000b: ldc.i4 stloc.0 0x0

// jump to the test IL_000c: br IL_0023

// execute the loop body IL_0011: ...

// increment the x variable by 1 IL_001b: IL_001c: IL_0021: IL_0022: ldloc.0 ldc.i4 add stloc.0 0x1

// TEST // load x, load 100, branch if // x is less than 100

IL_0023: IL_0024: IL_0029:

ldloc.0 ldc.i4 blt 0x64 IL_0011

Figure 13 Cdigo para el bucle For


else if (stmt is ForLoop) { // example: // var x = 0; // for x = 0 to 100 do // print "hello";

// end;

// x = 0 ForLoop forLoop = (ForLoop)stmt; Assign assign = new Assign(); assign.Ident = forLoop.Ident; assign.Expr = forLoop.From; this.GenStmt(assign); // jump to the test Emit.Label test = this.il.DefineLabel(); this.il.Emit(Emit.OpCodes.Br, test);

// statements in the body of the for loop Emit.Label body = this.il.DefineLabel(); this.il.MarkLabel(body); this.GenStmt(forLoop.Body);

// to (increment the value of x) this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[forLoop.Ident]); this.il.Emit(Emit.OpCodes.Ldc_I4, 1); this.il.Emit(Emit.OpCodes.Add); this.Store(forLoop.Ident, typeof(int));

// **test** does x equal 100? (do the test) this.il.MarkLabel(test); this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[forLoop.Ident]); this.GenExpr(forLoop.To, typeof(int)); this.il.Emit(Emit.OpCodes.Blt, body); }

El cdigo IL empieza con la inicial para la asignacin de contador de bucles For e inmediatamente pasa a la prueba de bucles For mediante la bifurcacin de instrucciones IL. Etiquetas como las que se enumeran a la izquierda del cdigo IL se usan para permitir que el tiempo de ejecucin sepa dnde bifurcarse para la instruccin siguiente. Mediante la instruccin blt (bifurcacin si menor que), el cdigo de prueba comprueba si la variable x es menor de 100. Si es as, el cuerpo del bucle se ejecuta, la variable X se incrementa y vuelve a ejecutarse la prueba. El cdigo del bucle For en la figura 13 genera el cdigo requerido para realizar las operaciones de asignacin e incremento en la variable del contador. Usa tambin el mtodo MarkLabel en ILGenerator para generar etiquetas en las que las instrucciones de bifurcacin pueden bifurcarse.

Conclusin... casi
Hemos visto la base de cdigo para un compilador .NET sencillo y hemos explorado parte de la teora subyacente. Este artculo pretende ser una introduccin al misterioso mundo de la creacin de compiladores. Aunque puede encontrar informacin muy valiosa en lnea, hay algunos libros que tambin recomiendo consultar. Recomiendo hacer una recopilacin de copias de: Compiling for the .NET Common Language Runtime por John Gough (Prentice Hall, 2001), Inside Microsoft IL Assembler por Serge Lidin (Microsoft Press,2002), Programming Language Pragmatics por Michael L. Scott (Morgan Kaufmann, 2000) y Compilers: Principles, Techniques, and Tools por Alfred V. Oho, Monica S. Lam, Ravi Sethi y Jeffrey Ullman (Addison Wesley, 2006). Todos estos libros cubren bastante bien lo ms fundamental que hay que entender para escribir su propio compilador de lenguaje. Sin embargo, no he acabado con mi explicacin. En cuanto a la codificacin ms importante, me gustara tratar algunos temas avanzados para que entienda ms cosas.

Llamada del mtodo Dynamic


Las llamadas a mtodos son la piedra angular de cualquier lenguaje informtico; sin embargo, existe todo un universo de llamadas que pueden realizarse. Los lenguajes ms nuevos como Python retrasan el enlace de un mtodo y la llamada hasta el ltimo minuto; es lo que se denomina llamada dinmica. Los lenguajes dinmicos ms conocidos como el Ruby, JavaScript, Lua e incluso Visual Basic, comparten todos este mismo patrn. Para que un compilador emita cdigo y realice una llamada del mtodo, debe tratar el nombre del mtodo como un smbolo y pasarlo a una biblioteca del tiempo de ejecucin que realizar el enlace y las operaciones de llamada de acuerdo con la semntica del idioma. Supongamos que desactivamos Option Strict en el compilador de Visual Basic 8.0. Las llamadas al mtodo se enlazarn ms tarde y el tiempo de ejecucin de Visual Basic realizar el enlace y la llamada durante el tiempo de ejecucin.

En lugar de que el compilador de Visual Basic emita una instruccin IL de llamada al mtodo Method1, emite una instruccin de llamada al mtodo en el tiempo de ejecucin de Visual Basic llamado CompilerServices.NewLateBinding.LateCall. Al hacerlo, transfiere un objeto (obj) y el nombre simblico del mtodo (Method1) junto con cualquier argumento de mtodo. El mtodo LateCall de Visual Basic busca entonces al mtodo Method1 en el objeto usando Reflection y, si lo encuentra, realiza una llamada a ese mtodo basada en Reflection:
Option Strict Off

Dim obj obj.Method1()

IL_0001: IL_0003: ...

ldloc.0 ldstr "Method1"

IL_0012: call , string, ...)

object CompilerServices.NewLateBinding::LateCall(object, ...

Uso de LCG para realizar un enlace posterior rpido


La llamada del mtodo basada en Reflection puede ser bastante lenta (consulte mi artculo "Reflection: Dodge Common Performance Pitfalls to Craft Speedy Applications" enmsdn.microsoft.com/msdnmag/issues/05/07/Reflection). Tanto el enlace del mtodo como la llamada de ste representan una gran cantidad de rdenes mucho ms lentas que una sencilla instruccin IL de llamada. CLR de .NET Framework 2.0 incluye una caracterstica denominada generacin de cdigo ligero (LCG) que puede usarse para crear cdigo de manera dinmica y unir el sitio de la llamada al mtodo mediante la instruccin IL de llamada, accin que resulta ser mucho ms rpida. Esto acelera notablemente la llamada del mtodo. Todava es necesario realizar la bsqueda del mtodo en un objeto, pero una vez que se ha encontrado, puede crearse un puente DynamicMethod y almacenarse en la memoria cach para cada repeticin de llamada. La figura 15 muestra una versin muy sencilla de un enlace posterior que realiza la generacin dinmica de cdigo de un mtodo puente. Primero busca en la memoria cach y comprueba si el sitio de la llamada ha sido consultado anteriormente. Si el sitio de la llamada se est ejecutando por primera vez, genera un DynamicMethod que devuelve un objeto y toma una matriz de objeto como argumento. El argumento de la matriz de objetos contiene el objeto de la sesin y los argumentos que debern usarse para realizar la llamada final al mtodo. El cdigo IL se genera para desempaquetar la matriz de objetos en la pila, de forma que se empieza con el objeto de la sesin y, a continuacin, con los argumentos. Se emite entonces una instruccin de llamada y el resultado de esa llamada se devuelve al destinatario. La llamada al mtodo puente de LCG se realiza a travs de un delegado, lo cual es muy rpido. Se encapsulan sencillamente los argumentos del mtodo puente en una matriz de objetos y, a continuacin, se realiza la llamada. La primera vez que se hace, el compilador JIT compila el mtodo dinmico y ejecuta IL, que, a su vez, llama al mtodo final. Este cdigo es bsicamente el que generara un antiguo compilador esttico enlazado. Empuja un objeto de la sesin a la pila, luego los argumentos y, finalmente, llama al mtodo mediante la instruccin de llamada. Es una manera sorprendente de retrasar la semntica hasta el ltimo

minuto, lo cual permite cumplir con la semntica de llamada de enlace posterior que encontramos en la mayora de lenguajes dinmicos.

El tiempo de ejecucin de lenguaje dinmico


Si tiene la intencin real de implementar un lenguaje dinmico en el CLR, entonces debe consultar el tema que trata el tiempo de ejecucin de lenguaje dinmico (DLR), el cual fue anunciado por el equipo de CLR a finales de abril. Comprende las herramientas y las bibliotecas que se necesitan para crear grandes lenguajes dinmicos que interoperen con .NET Framework y el ecosistema de otros lenguajes compatibles con .NET. Las bibliotecas ofrecen todo lo necesario para crear un lenguaje dinmico, incluidos una implementacin de alta ejecucin para abstracciones de lenguaje dinmico habituales (llamadas que usan un mtodo muy rpido de enlace en tiempo de ejecucin, interoperabilidad del sistema de tipos, etc.), un sistema de tipos dinmico, un AST compartido, compatibilidad con el bucle Read Eval Print (REPL), etc. Una ojeada ms profunda a DLR va ms all del mbito de este artculo, pero recomiendo explorar y consultar los servicios que ofrece para lenguajes dinmicos. Para obtener ms informacin acerca del DLR, visite el blog de Jim Hugunin (blogs.msdn.com/hugunin).

Вам также может понравиться