Академический Документы
Профессиональный Документы
Культура Документы
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>
<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.
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) {
// 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(); 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; }
// <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; }
// <stmt> ; <stmt> public class Sequence : Stmt { public Stmt First; public Stmt Second; }
/* <expr> := <string>
* * * */
// <string> := " <string_elem>* " public class StringLiteral : Expr { public string 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;
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();
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.
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++;
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;
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)
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);
// Go Compile this.GenStmt(stmt);
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); }
// 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) {
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
// increment the x variable by 1 IL_001b: IL_001c: IL_0021: IL_0022: ldloc.0 ldc.i4 add stloc.0 0x1
// 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.
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
minuto, lo cual permite cumplir con la semntica de llamada de enlace posterior que encontramos en la mayora de lenguajes dinmicos.