UAA – Sistemas Electrónicos 2 Compiladores Eduardo Serna-Pérez Análisis léxico (Scanner) La fase de rastreo (scanner), tiene las funciones de leer el programa fuente como un archivo de caracteres y dividirlo en tokens. Los tokens son las palabras reservadas de un lenguaje, secuencia de caracteres que representa una unidad de información en el programa fuente. En cada caso un token representa un cierto patrón de caracteres que el analizador léxico reconoce, o ajusta desde el inicio de los caracteres de entrada. De tal manera es necesario generar un mecanismo computacional que nos permita identificar el patrón de transición entre los caracteres de entrada, generando tokens, que posteriormente serán clasificados. Este mecanismo es posible crearlo a partir de un tipo especifico de maquina de estados llamado autómata finito. 2.1 Función del analizador léxico Es la primera fase de un compilador. Su principal función consiste en leer la secuencia de caracteres del programa fuente, carácter a carácter, y elaborar como salida la secuencia de componentes léxicos que utiliza el analizador sintáctico. El analizador sintáctico emite la orden al analizador léxico para que agrupe los caracteres y forme unidades con significado propio llamados componentes léxicos (tokens). Los componentes léxicos representan: o o o o o o Palabras reservadas: if, while, do, … Identificadores: variables, funciones, tipos definidos por el usuario, etiquetas, … Operadores: =, >, <, >=, <=, +, *, … Símbolos especiales: ;, ( ), { }, … Constantes numéricas. literales que representan valores enteros y flotantes. Constantes de carácter: literales que representan cadenas de caracteres. El analizador léxico opera bajo petición del analizador sintáctico devolviendo un componente léxico conforme el analizador sintáctico lo va necesitando para avanzar en la gramática. Los componentes léxicos son los símbolos terminales de la gramática. Suele implementarse como una subrutina del analizador sintáctico. Cuando recibe la orden “obtén el siguiente componente léxico”, el analizador léxico lee los caracteres de entrada hasta identificar el siguiente componente léxico. componente léxico programa fuente analizador léxico analizador sintáctico árbol de análisis sintáctico obtén siguiente componente léxico tabla de símbolos Además el analizador léxico es responsable de: o o o o Manejo de apertura y cierre de archivo, lectura de caracteres y gestión de posibles errores de apertura. Eliminar comentarios, espacios en blanco, tabuladores y saltos de línea. Inclusión de archivos y macros. Contabilizar número de líneas y columnas para emitir mensajes de error. Una de las ventajas de separar el análisis léxico y análisis sintáctico es que facilita la transportabilidad del traductor si se decide realizar cambios posteriores, por ejemplo cambiar las etiquetas begin-end por llaves de apertura y cierre { }. 1 UAA – Sistemas Electrónicos 2.2 Compiladores Eduardo Serna-Pérez Componentes léxicos, patrones y lexemas En la fase de análisis, los términos componentes léxicos (token), patrón y lexema se emplean con significados específicos. Un analizador léxico, inicialmente lee los lexemas y le asigna un significado propio. • componente léxico es la secuencia lógica y coherente de caracteres relativo a una categoría: identificador, palabra reservada, literales (cadena/numérica), operador o carácter de puntuación, además de que un componente léxico puede tener uno o varios lexemas. • patrón es una regla que genera la secuencia de caracteres que puede representar a un determinado componente léxico (expresión regular). • lexema es una cadena de caracteres que concuerda con un patrón que describe un componente léxico (valor de cadena). Ejemplo de una cadena de código: const pi = 3.1416; Lexemas const = pi 3.1416 “hola mundo” Componente léxico const relación identificador número literal Patrón const < o <= o = o <> o > o >= letra seguida de letras o números cualquier literal numérica caracteres entre comillas El analizador léxico recoge información sobre los componentes léxicos en sus atributos asociados. Los tokens influyen en las decisiones del análisis sintáctico, y los atributos, en la traducción de los tokens. En la practica los componentes léxicos suelen tener solo un atributo. Para efectos de diagnostico, puede considerarse tanto el lexema para un identificador como el numero de línea en el que se encontró por primera vez. Esta información puede ser almacenada en la tabla de símbolos para el identificador (estructura de datos). Para la cadena E=M*C**2 de ejemplo, los componentes léxicos y los valores de atributo asociado son: <identificador, atributo para el símbolo E> <op_asignacion> <identificador, atributo para el símbolo M> <op_multiplica> <identificador, apuntador al símbolo C> <op_exponente> <numero, atributo valor 2> Tome en cuenta que ciertas parejas no necesitan un valor de atributo. Los atributos relacionados con ese token deberán ser conservados y transferidos a alguna estructura de datos para que sean empleados en las siguientes etapas del análisis 2.3 Manejo de Buffer de entrada Existen algunos aspectos de eficiencia relacionados con el manejo de buffer. Primero se menciona un esquema de doble buffer de entrada que resulta útil como pre-análisis de la entrada para identificar los componentes léxicos. El segundo esquema introduce algunas técnicas útiles para aumentar la eficiencia del analizador léxico, empleando “centinelas”. Hay veces en que el analizador léxico necesita analizar previamente varios caracteres, además del lexema para un patrón, antes de poder anunciar una concordancia. Se pueden emplear muchos esquemas de manejos de buffer, pero, aquí tan solo se señalarán algunos principios básicos. 2 UAA – Sistemas Electrónicos Compiladores Eduardo Serna-Pérez Parejas de buffer – Se utiliza un buffer dividido en dos mitades de N (1024 ó 512) caracteres cada una, Se leen N caracteres de entrada en cada mitad del buffer con un orden de lectura del sistema, en vez de invocar una instrucción de lectura para cada carácter de entrada. Si quedan menos de N caracteres en la entrada, entonces se lee un carácter especial eof en el buffer después de los caracteres de entrada. Es decir, eof marca el final del archivo fuente y es distinto a cualquier carácter de la entrada. E = M * C * * ↑ Inicio lexema 2 eof ↑ frente Se mantienen dos apuntadores en el buffer de entrada. La cadena de caracteres entre los dos apuntadores es el lexema en curso. Al principio, los dos apuntadores apuntan al primer carácter del próximo lexema que hay que encontrar. Uno de ellos, apuntador delantero, examina hacia adelante hasta encontrar una concordancia con un patrón. Una vez determinado el siguiente lexema, el apuntador delantero se coloca en el carácter de su extremo derecho. Después de haber procesado el lexema, ambos apuntadores se colocan en el carácter situado inmediatamente después del lexema. Con este esquema se pueden considerar los comentarios y los espacios en blanco como patrones que no producen componentes léxicos. Centinela – Corrigiendo la deficiencia del método anterior para reducir la necesidad de dos pruebas para cada avance del apuntador delantero. Se amplia cada mitad del buffer para admitir un carácter centinela al final. De esta manera se vuelve mas eficiente el proceso de cargar del buffer, realizando una carga en cada mitad. E = M * \0 C * * ↑ inicio lexema 2 \0 ↑ frente eof El manejo de buffer es una labor puramente de programación y se recomienda realizarla de la manera que resulte mas sencilla, no es necesario apegarse a alguno de los dos métodos antes mencionados. 2.4 Jerarquía de Chomsky En el campo de las ciencias computacionales y específicamente en el área de los lenguajes formales la jerarquía de Chomsky es una clase de gramática formal, que describe de manera precisa un lenguaje. Las gramáticas formales o gramáticas suelen ser clasificadas en dos grandes tipos: las gramáticas generativas, que en base a un conjunto de reglas nos indican como formar cadenas del lenguaje. Y las gramáticas analíticas, que nos indican como esos miembros del lenguaje pertenecen a un orden especifico. Una gramática formal consiste de: o o o o Un conjunto finito de símbolos terminales Un conjunto finito de símbolos no terminales Un conjunto finito de reglas de producción que consiste de una secuencia de éstos símbolos Un símbolo inicial Un lenguaje formal o gramática formal es una secuencia de símbolos que podrían ser construidos aplicando reglas de producción para esas secuencias de símbolos, y podrían en casi todos los casos describir de manera precisa un lenguaje de programación. 3 UAA – Sistemas Electrónicos Compiladores Eduardo Serna-Pérez La jerarquía de Chomsky consiste de los siguientes niveles de gramáticas: o Gramáticas sin restricciones (Tipo-0), son capaces de generar todos los lenguajes reconocidos por una maquina de Turing. o Gramáticas de contexto sensitivo (Tipo-1), son lenguajes capaces de ser reconocidos por una maquina de Turing no determinista. o Gramáticas libres de contexto (Tipo-2), estos lenguajes son exactamente todos aquellos lenguajes que pueden ser reconocidos por un autómata de pila no-determinístico. Las gramáticas libres de contexto son la base teórica para la construcción de la sintaxis en la mayoría de los lenguajes de programación. o Gramáticas regulares (Tipo-3), son exactamente todos los lenguajes que pueden ser reconocidos por un autómata de estado finito. Esta familia de lenguajes formales puede ser obtenida a partir de una expresión regular. Las expresiones regulares son comúnmente usadas para realizar la búsqueda de patrones en estructuras léxicas de lenguajes de programación. 2.5 Especificaciones de componentes léxicos Las expresiones regulares son una notación importante para especificar patrones. Cada patrón concuerda con una serie de cadenas que, de modo que las expresiones regulares servirán como nombres para conjuntos de cadenas. El término alfabeto denota cualquier conjunto finito de símbolos, por ejemplo el alfabeto binario { 0, 1 }. Una cadena es una secuencia de finita de símbolos de un alfabeto y un lenguaje se refiere a conjunto de cadenas de un alfabeto fijo. 2.5.1 Expresiones Regulares El origen de las expresiones regulares surge de la teoría de autómatas y la teoría de lenguajes formales, ambas parte de la ciencias computacionales teórica. Este campo estudia los modelos computacionales (autómata) y la manera de describir y clasificar los lenguajes formales. Un lenguaje formal puede ser especificado de varias maneras, tales como: • • • Cadenas producidas por alguna gramática formal. Cadenas producidas por expresiones regulares. Cadenas aceptadas por algunos autómatas tales como las maquinas de Turing o autómatas de estado finito. A las expresiones regulares frecuentemente se les llaman patrones, ya que son expresiones que describen a un conjunto de cadenas. Frecuentemente son usadas para dar una descripción concisa de un conjunto, sin tener que listar todos sus elementos. Las expresiones regulares pueden ser expresadas en términos de la teoría de lenguajes formales. Consisten de constantes y operadores que denotan el conjunto de cadenas y operaciones sobre estos conjuntos, respectivamente. Dado un alfabeto Σ las siguientes constantes son definidas: o o o Conjunto vacío: L( ∅ ) denota el conjunto { } Cadena vacía: L( ε ) denota el conjunto { ε } Carácter del alfabeto: L( a ), a elemento de Σ denota el conjunto { “a” } Operaciones básicas en expresiones regulares Selección de alternativas (unión) – Si r y s son expresiones regulares, entonces r | s es una expresión regular que define cualquier cadena que concuerda con r o con s. En términos de lenguajes decimos que r | s es la unión de los lenguajes de r y s, o L(r | s) = L(r) U L(s) = { αβ | α esta en r o β esta en s }. 4 UAA – Sistemas Electrónicos Compiladores Eduardo Serna-Pérez ejemplo: o o o L(a | b) = L(a ) U L(b) = { a, b } L(a | ε ) = { a, ε } L(a | b | c | d ) = { a, b, c, d } Concatenación – La concatenación de dos expresiones regulares r y s se escribe como rs (yuxtaposición) y corresponde a cualquier cadena que sea la concatenación de dos cadenas, con la primera de ellas correspondiendo a r y la segunda a s. Por ejemplo la expresión regular ab corresponde a la cadena { ab }, mientras que la expresión regular (a | b)c corresponde a las cadenas { ac, bc }. De esta forma la operación de concatenación para expresiones regulares se puede definir como L(rs) = L(r) L(s) = { αβ | α esta en r y β esta en s }. ejemplo: o o L(a | b) c = L(a | b) L(c) = { a, b } {c} = { ac, bc } L(ab|c)(d|ef)=L(ab|c)L(d|ef)={ab,c}{d,ef}={abd, abef, cd, cef} Repetición (cerradura de Kleene) – Se escribe r*, donde r corresponde a la expresión regular. La expresión regular r* corresponde a cualquier concatenación finita de cadenas, cada una de las cuales corresponde a r. Por ejemplo, la expresión regular a* corresponde a las cadenas { ε, a, aa, aaa, … }, (concuerda con ε por que ε es la concatenación de ninguna cadena concordante con a). En términos de lenguaje podemos decir que: ∞ S* = U S n n =0 Donde Sn = S … S es la concatenación de S n veces (S0 = { ε }). ejemplo: o o o L(a|b)* = {a, bb}* = {ε,a,bb,aa,abb,bba,bbbb,aaa,abba,abbbb,...} L(a | b*) = {a, b*} = { ε, a, b, bb, bbb, ...} L(ab|c)* = {ab,c}* = {ε, ab, c, abab, abc, cab, cc, ababab,...} Para evitar los paréntesis se asume que la repetición tiene la precedencia mas alta, luego la concatenación y al final la unión. Si no existiera ambigüedad los paréntesis pueden ser omitidos . Por ejemplo , (ab)c se escribe como abc y a|(b(c*)) puede ser escrito como a|bc*. De manera que los lenguajes regulares deben su nombre al hecho de que presentan “regularidades” o repeticiones de los mismo componentes, como por ejemplo el lenguaje L1: L1={ab, abab, ababab, abababab, ...} Así pues, una expresión regular se construye a partir de expresiones regulares mas simples utilizando un conjunto de reglas definitorias (operaciones básicas). Cada expresión regular r representa un lenguaje L(r). Las reglas de definición especifican como se forma L(r) combinando de varias maneras los lenguajes representados por las subexpresiones de r. Se dice que un lenguaje designado por una expresión regular es un conjunto regular. Es importante recordar que la especificación de una expresión regular es un ejemplo de definición recursiva. Definición regular: Es una forma de simplificación, dando un nombre a las expresiones regulares y definiendo nuevas expresiones regulares utilizando dichos nombres como si fueran símbolos. Por ejemplo, podríamos desarrollar una expresión regular para una secuencia de uno o mas dígitos, generando inicialmente una definición regular para un digito. digito = 0 | 1 | 2 | … | 9 (definición regular) digito digito* (expresión regular para números enteros sin signo) 5 UAA – Sistemas Electrónicos Compiladores Eduardo Serna-Pérez Ahora estamos en posición de elaborar la una definición de expresiones regulares para la identificación de los componentes de un lenguaje determinado. Los tokens de lenguajes de programación tiende a caer dentro de varias categorías limitadas que son bastante estandarizadas, como palabras reservadas, símbolos especiales, identificadores y literales (numérica/cadena). Por ejemplo los identificadores en la mayoría de los lenguajes de programación es el conjunto de cadenas de letras y dígitos que empiezan con una letra. Para ello generamos las definiciones regulares para letra y digito: letra = a | b | c | … | z | A | B … | Z digito = 0 | 1 | 2 | … | 9 identificador = letra ( letra | digito )* Los números con signo son literales numéricas constituidas por el punto decimal después de un digito y por una literal que represente la parte del exponente. Con esta información desarrollamos las definiciones regulares y las expresiones regulares necesarias: ((+|–|ε)digito digito*) ((. digito)|ε) ((E(+|–|ε)digito)|ε) entero = (+|–|ε) digito digito* fracción = ( . digito ) | ε exponente = ( E ( + | – | ε ) digito ) | ε numero = digito fracción exponente Esta definición establece que fracción es un punto decimal seguido de uno o mas dígitos, o esta ausente. Un exponente, que es E seguido de un signo + ó - ó ausente, seguidos de uno o mas dígitos, o la ausencia de exponente. Tome en cuanta que, como mínimo debe existir un dígito después del punto. De manera que numero concuerda con 1 y con 1.0. Abreviaturas en la notación o Uno o mas casos – el operador unitario postfijo + significa “uno o mas casos de”, de manera + que la expresión regular r que designa al lenguaje L(r), entonces r es una expresión que + + designa al lenguaje L(r) . Así la expresión regular a representa al conjunto de todas las cadenas de una o mas a. El operador + tiene la misma precedencia que la repetición, las identidades algebraicas son r* = r | ε y r + = r r*. o Cero o un caso – el operado unitario postfijo ? significa “cero o un caso de”. La notación r? es la abreviatura de r | ε. Si r es una expresión regular, entonces (r)? es una expresión regular que designa el lenguaje L(r) U { ε }. o Clases de caracteres – una clase abreviada de caracteres como [a – z] designa la expresión regular a | b | c | … | z. A menudo en la descripción de los tokens de lenguaje de programación utilizamos expresiones regulares, algunas cadenas se pueden definir mediante varias expresiones regulares diferentes, Por ejemplo, cadenas tales como if y while podrían se identificadores o palabras clave. Una definición de lenguaje de programación debe establecer cual interpretación se observará, y las expresiones regulares por si mismas no pueden hacer esto. En realizad, una definición de lenguaje debe proporcionar reglas de de no ambigüedad que explicaran cual significado es el conveniente para cada uno de tales casos. Existen dos reglas básicas que suponen tales casos: o La primera establece que, cuando una cadena puede ser identificador o palabra clave, se prefiere por lo general la interpretación como palabra clave. 6 UAA – Sistemas Electrónicos o Compiladores Eduardo Serna-Pérez La segunda establece que, cuando una cadena puede ser un token simple o una secuencia de varios tokens, por lo común se prefiere la interpretación del token simple. Esta preferencia se conoce a menudo como el principio de sub-cadena mas larga. Una cuestión que surge en el principio de sub-cadena mas larga es lo referente a los delimitadores de tokens, o caracteres que implican que una cadena mas larga en el punto donde aparecen no pueden representar un token. Los caracteres que son parte no ambigua de otros tokens son delimitadores. Por ejemplo xtemp=ytem, el signo de = sirve como delimitador pues no forma parte de los identificadores. Los espacios en blanco, los retornos de línea y los caracteres de tabulación generalmente se asumen como delimitadores de token. De esta manera el reconocimiento de componentes léxicos podría realizarse partiendo del conjunto de cadenas dadas por expresiones regulares. Por ejemplo: if = if else = else oprelacion = < | <= | = | <> | > | >= identificador = letra ( letra | digito )* entero = ( + | – )? digito+ real = ( + | – )? digito+ ( . digito+ )? ( E ( + | – )? digito+ )? delimitador = blanco | tab | nuevalínea Para este fragmento de lenguaje, el analizador léxico reconocerá las palabras clave if, else, al igual que los lexemas representados por opRelacion, identificador, numero y delimitador. Aún cuando las expresiones regulares son un mecanismo poderoso de definición de lenguajes, es necesario construir una herramienta computacional que nos permita su implementación, para ello se emplean los autómatas de estado finito. Es decir que, las expresiones regulares deberán ser transformadas a algún tipo de maquina de estados fácil de programar. 2.6 Reconocimiento de componentes léxico Un reconocedor de lenguajes es un programa que toma como entrada una cadena x e indica si dicha cadena pertenece a una frase del programa. Se compila una expresión regular en un reconocedor construyendo un diagrama de transiciones generalizado llamado autómata finito. Un autómata finito puede ser determinista o no determinista, dependiendo del número de transiciones para un mismo símbolo de entrada. Tanto los autómatas finitos deterministas como los no deterministas pueden reconocer con precisión a los conjuntos regulares. Por tanto, ambos pueden reconocer con precisión lo que denotan las expresiones regulares. 2.6.1 Autómatas finitos Una maquina de estado finito o autómata finito, es un modelo computacional que consiste de un conjunto de estados, un estado de inicio, un alfabeto de entrada y una función de transición que traza un mapa a un siguiente estado, a partir del símbolo de entrada y el estado actual. En particular, los autómatas finitos se pueden utilizar para describir el proceso de reconocimiento de patrones en cadenas de entrada. El sistema recibe una cadena constituida por símbolos de un alfabeto y determina si esa cadena pertenece al lenguaje que ese autómata reconoce. De esta manera se pueden construir analizadores léxicos, construyendo programas de computadora que realicen las operaciones de un autómata. Es claro que existe una fuerte relación entre los autómatas y las expresiones regulares, y veremos mas adelante como construir un autómata finito a partir de una expresión regular. 2.6.2 Autómata finito no determinístico 7 UAA – Sistemas Electrónicos Compiladores Eduardo Serna-Pérez 2.6.3 Autómata finito determinístico Un autómata finito determinista (DFA por sus siglas en ingles), es un modelo donde el siguiente estado de la transición esta dado particularmente por el estado actual y el carácter de entrada actual. Si ningún estado de transición es especificado, la cadena entrante es rechazada. La siguiente definición formalmente introduce un DFA, ( S, Σ, T, s, A ) donde: o o o o o S conjunto finito no vacío de elementos llamado estados Σ alfabeto de entrada T es una función de transición de S × Σ en S s ∈ S estado inicial A ⊆ S conjunto no vacío de estados finales El ADF se encuentra en un estado inicial y lee una entrada de cadena de caracteres de izquierda a derecha. La función de transición T define los estados de transición y es denotada por T ( si ,c ) = si+1, donde si y si+1 son estados de S, y c es un carácter del alfabeto de entrada. La función de transición indica que cuando un autómata esta en un estado si y recibe el siguiente símbolo de entrada c, el autómata deberá cambiar al estado si+1. El carácter de entrada no causara transición en caso de ser un carácter vació. El siguiente ejemplo nos muestra un autómata que reconoce cadenas de lenguaje para una expresión regular ( a | b )* abb. El conjunto de estados del DFA es { 0, 1, 2, 3 } y el alfabeto de símbolos de entrada es { a, b }. Se considera 0 como el estado de inicio, y 3 el estado de aceptación que esta indicado mediante un doble circulo. La siguiente función define dicho autómata: F = ({ 0, 1, 2, 3 }, { a, b }, T, 0, { 3 } ) Donde T es la tabla de transición que refleja el patrón de la expresión regular que estamos evaluando T ( si ,c ) = si+1 definida como: Símbolos de entrada a b 1 0 1 2 1 3 1 0 Estado 0 1 2 3 La tabla de transición también puede ser representada mediante un grafo de transición, donde cada nodo representa los posibles estados y las líneas reflejan el flujo de la transición que registran un cambio de un estado a otro en una coincidencia de carácter el estado 0 es considerado el inicio de la transición y el estado 3 (doble circulo) representa el fin de la transición o estado de aceptación: a a a 0 b b b 1 2 a 3 b Justamente el autómata que se acaba de diseñar es capas de aceptar cadenas de entrada que pertenezcan a la expresión regular dada anteriormente, es decir, { abb, aabb, babb, ababb, baabb, aaabb, … }. Cualquier cadena de entrada que no corresponda con dicho patrón será considerada como ajena al lenguaje, es decir, terminar en cualquier punto que no sea 3 (fin de la transición) es considerado como un posible error. Igualmente es posible diseñar un algoritmo que realice exactamente las mismas operaciones que el autómata dado anteriormente para la expresión regular ( a | b )* abb. Así pues, 8 UAA – Sistemas Electrónicos Compiladores Eduardo Serna-Pérez consideremos que la función de transición (siguiente estado) desde un estado s en un carácter de entrada c, esta dada por una operación de selección de estado (case en s). Además consideraremos la existencia de una función sigtecar() que devuelve el siguiente carácter de la cadena de entrada, obteniendo el siguiente algoritmo: s = 0; c = sigtecar( cadena ); while ( c != ‘\0’ ) { case ( s ) { 0: s = ( c == ‘b’ )? 0 : 1: s = ( c == ‘b’ )? 2 : 2: s = ( c == ‘b’ )? 3 : 3: s = ( c == ‘b’ )? 0 : } c = sigtecar( cadena ) } if ( s == 3 ) “cadena valida” 1 1 1 1 Dicho algoritmo es capas de evaluar una entrada de cadena y determinar si la misma forma parte del patrón especificado por la expresión regular dada. Si el algoritmo llega a un fin de cadena con un estado de aceptación 3, quiere decir que dicha cadena es valida o concuerda con el patrón. Ahora estamos en posición de poder construir autómatas mas elaborados partiendo de las expresiones regulares ya definidas, por ejemplo, pensemos en la definición regular para un identificador que esta compuesta por la siguiente expresión regular letra ( letra | digito )*, para este caso consideráremos el conjunto de estados como 2 { 0, 1 }, el alfabeto de entrada estará compuesto de las definiciones regulares letra y digito. El 0 es considerado como el inicio y 1 como el fin de la transición. De esta manera generamos el siguiente diagrama de transición para un identificador: letra letra 0 1 digito Bibliografía Aho, A.V., Sethi, R., Ullman, J.D. (1990), Compiladores: principios, técnicas y herramientas, capitulo 1, páginas: 1- 25, 743-747. Louden, K.C. (1997), Construcción de Compiladores: Principios y práctica, capitulo 1, páginas: 1- 27. http://en.wikipedia.org/wiki/Compiler 9