2.1 El proceso de análisis léxico Un compilador se compone internamente de varias etapas, o fases, que realizan distintas operaciones lógicas. Es útil pensar en estas fases como en piezas separadas dentro del compilador, y pueden en realidad escribirse como operaciones codificadas separadamente aunque en la práctica a menudo se integren juntas. El análisis léxico constituye la primera fase, aquí se lee el programa fuente de izquierda a derecha y se agrupa en '''componentes léxicos''' (Token (programación)), que son secuencias de caracteres que tienen un significado. Además, todos los espacios en blanco, líneas en blanco, comentarios y demás información innecesaria se elimina del programa fuente. También se comprueba que los símbolos del lenguaje (palabra clave, operador, ...) se han escrito correctamente. Como la tarea que realiza el analizador léxico es un caso especial de coincidencia de patrones, se necesitan los métodos de especificación y reconocimiento de patrones, y éstos métodos son principalmente las expresión regular y los autómata finito. Sin embargo, un analizador léxico también es la parte del compilador que maneja la entrada del código fuente, y puesto que esta entrada a menudo involucra un importante gasto de tiempo, en analizador léxico debe funcionar de manera tan eficiente como sea posible. 2.2 Expresiones regulares Una expresión regular, a menudo llamada también '''patrón''', es una expresión que describe un conjunto de cadenas sin enumerar sus elementos. Por ejemplo, el grupo formado por las cadenas ''Handel'', ''Händel'' y ''Haendel'' se describe mediante el patrón "H(a|ä|ae)ndel". La mayoría de las formalizaciones proporcionan los siguientes constructores: una '''expresión regular''' es una forma de representar a los lenguaje regular (finitos o infinitos) y se construye utilizando carácter|caracteres del alfabeto sobre el cual se define el lenguaje. Específicamente, las expresiones regulares se construyen utilizando los operadores unión concatenación y clausura de Kleene. Alternación Una barra vertical separa las alternativas. Por ejemplo, "marrón|castaño" casa con ''marrón'' o ''castaño''. Cuantificación Un cuantificador tras un carácter especifica la frecuencia con la que éste puede ocurrir. Los cuantificadores más comunes son +, ? y *: + El signo más indica que el carácter al que sigue debe aparecer al menos una vez. Por ejemplo, "ho+la" describe el conjunto infinito ''hola'', ''hoola'', ''hooola'', ''hoooola'', etcétera. ? El signo de interrogación indica que el carácter al que sigue puede aparecer como mucho una vez. Por ejemplo, "ob?scuro" casa con ''oscuro'' y ''obscuro''. agrupación Los paréntesis pueden usarse para definir el ámbito y precedencia de los demás operadores. Por ejemplo, "(p|m)adre" es lo mismo que "padre|madre", y "(des)?amor" casa con amor y con desamor. Los constructores pueden -6- combinarse libremente dentro de la misma expresión, por lo que "H(ae?|ä)ndel" equivale a "H(a|ae|ä)ndel". La sintaxis precisa de las expresiones regulares cambia según las herramientas y aplicaciones consideradas, y se describe con más detalle a continuación. Su utilidad más obvia es la de describir un conjunto de cadenas, lo que resulta de utilidad en editores de texto y aplicaciones para buscar y manipular textos. Muchos lenguajes de programación admiten el uso de expresiones regulares con este fin. Por ejemplo, Perl tiene un potente motor de expresiones regulares directamente incluido en su sintaxis. Las herramientas proporcionadas por las distribuciones de Unix (incluyendo el editor sed y el filtro grep) fueron las primeras en popularizar el concepto de expresión regular. Numerosos editores de texto y otras utilidades (especialmente en el sistema operativo UNIX/linux), como por ejemplo sed y awk, utilizan expresiones regulares para, por ejemplo, buscar palabras en el texto y reemplazarlas con alguna otra cadena de caracteres. En el área de la programación las expresiones regulares son un método por medio del cual se pueden realizar búsquedas dentro de cadenas de caracteres. Sin importar si la búsqueda requerida es de dos caracteres en una cadena de 10 o si es necesario encontrar todas las apariciones de un patrón definido de caracteres en un archivo de millones de caracteres, las expresiones regulares proporcionan una solución para el problema. Adicionalmente, un uso derivado de la búsqueda de patrones es la validación de un formato específico en una cadena de caracteres dada, como por ejemplo fechas o identificadores. Para poder utilizar las expresiones regulares al programar es necesario tener acceso a un motor de búsqueda con la capacidad de utilizarlas. Es posible clasificar los motores disponibles en dos tipos: Motores para el programador y Motores para el usuario final. Motores para el usuario final: Son programas que permiten realizar búsquedas sobre el contenido de un archivo o sobre un texto extraído y colocado en el programa. Están diseñados para permitir al usuario realizar búsquedas avanzadas usando este mecanismo, sin embargo es necesario aprender a redactar expresiones regulares adecuadas para poder utilizarlos eficientemente. Éstos son algunos de los programas disponibles: grep: Programa de los sistemas operativos Unix/Linux PowerGrep: versión de grep para los sistemas operativos Windows RegexBuddy: Ayuda a crear las expresiones regulares en forma interactiva y luego le permite al usuario usarlas y guardarlas. EditPad Pro: Permite realizar búsquedas con expresiones regulares sobre archivos y las muestra por medio de código de colores para facilitar su lectura y comprensión. Motores para el programador: Permiten automatizar el proceso de búsqueda de modo que sea posible utilizarlo muchas veces para un propósito específico. Estas son algunas de las herramientas de programación disponibles que ofrecen motores de búsqueda con soporte a expresiones regulares: Java: Existen varias librerías hechas para java que permiten el uso de RegEx, y Sun planea dar soporte a estas desde el SDK JavaScript: A partir de la versión 1.2 (ie4+, ns4+) JavaScript tiene soporte integrado para expresiones regulares, lo que significa que las validaciones que se -7- realizan normalmente en una página web podrían simplificarse grandemente si el programador supiera utilizar esta herramienta. Perl: Es el lenguaje que hizo crecer a las expresiones regulares en el ámbito de la programación hasta llegar a lo que son hoy en día. PCRE: Librería de ExReg para C, C++ y otros lenguajes que puedan utilizar librerías dll (Visual Basic 6 por ejemplo). PHP: Tiene dos tipos diferentes de expresiones regulares disponibles para el programador. Python: Lenguaje de "scripting" popular con soporte a Expresiones Regulares. .Net Framework: Provee un conjunto de clases mediante las cuales es posible utilizar expresiones regulares para hacer búsquedas, reemplazar cadenas y validar patrones. Nota: De las herramientas mencionadas con anterioridad se utilizan el EditPad Pro y el .Net Framework para dar ejemplos, aunque es posible utilizar las expresiones regulares con cualquier combinación de las herramientas mencionadas. Aunque en general las Expresiones Regulares utilizan un lenguaje común en todas las herramientas, las explicaciones prácticas acerca de la utilización de las herramientas y los ejemplos de código deben ser interpretados de forma diferente. También es necesario hacer notar que existen algunos detalles de sintaxis de las expresiones regulares que son propietarios del .Net Framework que se utilizan en forma diferente en las demás herramientas de programación. Cuando estos casos se den se hará notar en forma explícita para que el lector pueda buscar información respecto a estos detalles en fuentes adicionales. En el futuro se incluirán adicionalmente ejemplos de otras herramientas y lenguajes de programación. Expresiones Regulares como motor de búsqueda Las expresiones regulares permiten encontrar porciones específicas de texto dentro de una cadena más grande de caracteres. Así, si es necesario encontrar el texto "lote" en la expresión "el ocelote salto al lote contiguo" cualquier motor de búsqueda sería capaz de efectuar esta labor. Sin embargo, la mayoría de los motores de búsqueda encontrarían también el fragmento "lote" de la palabra "ocelote", lo cual podría no ser el resultado esperado. Algunos motores de búsqueda permiten adicionalmente especificar que se desea encontrar solamente palabras completas, solucionando este problema. Las expresiones regulares permiten especificar todas estas opciones adicionales y muchas otras sin necesidad de configurar opciones adicionales, sino utilizando el mismo texto de búsqueda como un lenguaje que permite enviarle al motor de búsqueda exactamente lo que deseamos encontrar en todos los casos, sin necesidad de activar opciones adicionales al realizar la búsqueda. Expresiones Regulares como lenguaje Para especificar opciones dentro del texto a buscar se utiliza un lenguaje o convención mediante el cual se le transmite al motor de búsqueda el resultado que se desea obtener. Este lenguaje le da un significado especial a una serie de caracteres. Por lo tanto cuando el motor de búsqueda de Expresiones Regulares encuentre estos caracteres no los buscará en el texto en forma literal, sino que buscará lo que los caracteres significan. A estos caracteres se les llama algunas veces "meta-caracteres". A continuación se listan los principales meta-caracteres y su función y como los interpreta el motor de Expresiones Regulares. -8- 2.3 Autómatas finitos Un autómata finito o máquina de estado finito es un modelo matemático de un sistema que recibe una cadena constituida por símbolos de un alfabeto y determina si esa cadena pertenece al lenguaje que el autómata reconoce. Formalmente, un autómata finito (AF) puede ser descrito como una 5-tupla (S,Σ,T,s,A) donde: S un conjunto de estados; Σ es un alfabeto; T es la función de transición: ; es el estado inicial; es un conjunto de estados de aceptación o finales. Ejemplo 1 S = {S1, S2}, Σ = {0,1}, T = {(S1,0,{S2});(S1,1,{S1});(S2,0,{S1});(S2,1,{S2})} s = S1 A = {S1}. Además de notar un AF a través de su definición formal es posible representarlo a través de otras notaciones que resultan más cómodas. Entre estas notaciones, las más usuales son: Las Tablas de Transiciones: la tabla de transición para el AF del ejemplo 1 es 0 1 S S S 1 2 1 S S S 2 1 2 Los Diagramas de Transiciones: el diagrama de transición para el AF del ejemplo 1 es Figura 2.1 -9- Las Expresiones regulares. Se demuestra que dado un autómata de estados finitos, existe una expresión regular que lo representa. Ø υ 1* υ (1* ο 0 ο 1* ο 0)*. En el comienzo del proceso de reconocimiento de una cadena, el AF se encuentra en el estado inicial y a medida que procesa cada símbolo de la cadena va cambiando de estado de acuerdo a lo determinado por la función de transición. Cuando se ha procesado el último de los símbolos de la cadena de entrada, el autómata se detiene. Si el estado en el que se detuvo es un estado de aceptación o final, entonces la cadena pertenece al lenguaje reconocido por el autómata, caso contrario, la cadena no pertenece a dicho lenguaje. 2.4 Desde las expresiones regulares hasta los DFA ( autómatas finitos determinstas) Un AFD o autómata finito determinista es aquel autómata finito cuyo estado de llegada está unívocamente determinado por el estado inicial y el carácter leído por el autómata. Formalmente, un autómata finito determinista (AFD) es similar a un Autómata de estados finitos, representado con una 5-tupla (S,Σ,T,s,A) donde: Σ es un alfabeto; S un conjunto de estados; T es la función de transición: ; es el estado inicial; es un conjunto de estados de aceptación o finales. Al contrario de la definición de autómata finito, este es un caso particular donde no se permiten transiciones vacías, el dominio de la función T es S (con lo cual no se permiten transiciones desde un estado de un mismo símbolo a varios estados). A partir de este autómata finito es posible hallar la expresión regular resolviendo un sistema de ecuaciones. S1 = 1 S 1 + 0 S 2 + ε S2 = 1 S 2 + 0 S 1 Siendo ε la palabra nula. Resolviendo el sistema y haciendo uso de las reducciones apropiadas se obtiene la siguiente expresión regular: 1*(01*01*)*. Inversamente, dada la expresión regular es posible generar un autómata que reconozca el lenguaje en cuestión utilizando el algoritmo de Thompson, desarrollado por Ken Thompson, uno de los principales creadores de UNIX, junto con Dennis Ritchie. Un tipo de autómatas finitos deterministas interesantes son los tries. Autómatas finitos no deterministas Un AFND o autómata finito no determinista es aquel que presenta cero, una o más transiciones por el mismo carácter del alfabeto. Un autómata finito no determinista también puede o no tener más de un nodo inicial. - 10 - Los AFND también se representan formalmente como tuplas de 5 elementos (S,Σ,T,s,A). La única diferencia respecto al AFD es T. AFD: AFND: (partes de S) Debido a que la función de transición lleva a un conjunto de estados, el automáta puede estar en varios estados a la vez (o en ninguno si se trata del conjunto vacío de estados). Autómatas finitos no deterministas con transiciones vacías Un AFND-ε o autómata finito no determinista con transiciones ε permite cambiar de estado sin procesar ningún símbolo de entrada. Cuando el autómata llega a un estado, se encuentra en ese estado y en los estados a los que apunte este mediante una transición ε. Un automata es un AFND: (partes de S) AFND-ε: (partes de S) Cuando el símbolo de entrada es la palabra vacía (ε), existe una transición ε entre los estados. AFD, AFND y AFND-ε Para todo AFND-ε existe un AFND equivalente y para todo AFND existe un AFD equivalente. Existen algoritmos para transformar un autómata en otro. Los AFD son los más sencillos de construir, por tanto, puede ser útil diseñar un autómata complejo como AFND-ε o AFND para luego transformarlo en AFD para su implementación. 2.5 Implementación de un analizador léxico TINY (“Diminuto”) Con las herramientas y los conceptos expuestos en los párrafos anteriores, parecen ser los suficientes para implementar un analizador léxico pequeño. Los pasos a seguir serian enumerados de la siguiente manera: � Determinar los tokens un patrón para cada uno � Implementar los patrones � Manual � Herramientas � Definir La función de un analizador léxico es examinar el código fuente y reconocer las palabras o tokens que formen parte del lenguaje así como determinar si hay elementos que no pertenecen al lenguaje. Como resultado del análisis léxico se devolverá la lista de tokens que forman el código fuente. Lo primero que debemos hacer es definir los tipos de palabras que compondrán el lenguaje así como las reglas para formar palabras. En el lenguaje que estoy creando he identificado los siguientes tipos de palabras: - 11 - Identificadores Cadenas Valores o constantes numéricas Valores o constantes lógicas Palabras reservadas Símbolos especiales como “,”, “(” y “)” Operadores Saltos de línea Fin de fichero Dicho lenguaje pretende ser bastante sencillo y fácil de utilizar. Por ello se utilizan palabras en castellano en vez de en ingles como la mayoría de lenguajes de programación. Ahora pasaremos a definir las reglas de formación de cada uno de esos tipos de palabras. Hay tipos de palabras que se definen por enumeración como son los operadores, los símbolos especiales, las palabras reservadas, etc. Para definir otros tipos de tokens utilizaremos autómatas finitos deterministas como ya se explico en el artículo Autómata finito determinista para reconocer constantes numéricas. Definiciones Operadores: o + , -, *, /, =, !=, <, >, >=, <=, Y, O Palabras reservadas: Variable: Constante: Logica: Cadena: Numerica: Si: Entonces: Fin: Mientras: para declarar variables para declarar constantes tipo de dato tipo de dato tipo de dato Forma parte de la estructura condicional Forma parte de la estructura Condicional Forma parte de la estructura Condicional e Iterativa Forma parte de la estructura Iterativa Símbolos especiales: (, ) y , Constantes lógicas: Verdadero, Falso Constante cadena: Secuencia de caracteres delimitados por las comillas (“) Si la cadena contiene el carácter “ o \ debe ir precedido del carácter de escape \ - 12 - Figura 2.2 A.F.D. Reconocedor de cadenas. Constante numérica: Una constante numérica debe estar formada por al menos un dígito, puede ir precedida o no por el signo y puede tener o no decimales. Si contiene el punto decimal debe tener al menos un dígito en la parte de los decimales. Figura 2.3 A.F.D. Reconocedor de constantes numéricas. Identificadores: Una letra seguida una secuencia de 0 o más letras o dígitos o el signo _ - 13 - Figura 2.4 A.F.D. Reconocedor de identificadores. Trás definir todos los posibles tokens procederemos a la implementación del analizador léxico que los reconocerá. El analizador antes de poder reconocer los tokens debe fraccionar el código fuente en las palabras que lo componen, teniendo en cuenta para ello los separadores (salto de línea y espacio en blanco). Además se debe tener en cuenta que los espacios en blanco dentro de una cadena no deben ser tenidos en cuenta. Una vez separadas todas las palabras existentes en el código fuente se deben intentar reconocer cada una de ellas. Para ello nos serviremos de autómatas finitos deterministas, que desarrollaremos según lo expuesto antes, para los componentes léxicos más complejos. Para aquellas palabras cuyo reconocimiento es trivial, como los operadores o las palabras reservadas, simplemente compararemos con dichas elementos. Como se muestra en el siguiente fragmento de código que es utilizado para reconocer si una palabra es una constante lógica o no. 1. 2. 3. Private Shared Function EsConstanteLogica(ByVal palabra As String) As Boolean Return palabra.ToUpper = "VERDADERO" Or palabra.ToUpper = "FALSO" End Function El analizador recibe como parámetros la ruta de un fichero que contiene el script, una cadena con el script o un array de cadenas conteniendo cada una línea del script. Como salida genera una colección de palabras o componentes léxicos. Para cada uno de ellos se proporciona la siguiente información: Palabra en si tal y como aparece en el script Tipo de componente léxico Componente sintáctico con que se corresponde y que será utilizado posteriormente por el analizador sintáctico Linea y columna del script en la que aparece la palabra. Utilizado para mostrar los errores resaltándolos en el código del script y notificar en que punto se ha producido el posible error. El analizador reconoce los siguientes componentes léxicos: Identificador CTE_Cadena CTE_Numerica CTE_Logica Palabra_Reservada Simbolo_Especial ",", "(" y ")" Operador Salto_Linea Fin_Fichero - 14 - Erronea La siguiente entrada del analizador léxico: n1 "hola" "hño l!'a" "\\as" "ac\\as\"11\"22\"dd fas" c2 fin variable 3 * a verdadero Y fas"dfas "as"asf" "a\aas" 1234.34 c3 c3 "AAAA BB CC" "fasdfs\" numerica 3 / 456 <> != 3 O fadvariable c3 si falso cadena ( , 3 ) produce la siguiente salida: Salida del analizador léxico. - 15 - 2.6 Uso de Lex para generar automáticamente un analizador léxico El lex es un generador de programas diseñado para el proceso léxico de cadenas de caracteres de input. El programa acepta una especificación, orientada a resolver un problema de alto nivel para comparar literales de caracteres, y produce un programa C que reconoce expresiones regulares. Estas expresiones las especifica el usuario en las especificaciones fuente que se le dan al lex. El código lex reconoce estas expresiones en una cadena de input y divide este input en cadenas de caracteres que coinciden con las expresiones. En los bordes entre los literales, se ejecutan las secciones de programas proporcionados por el usuario. El fichero fuente lex asocia las expresiones regulares y los fragmentos de programas. Puesto que cada expresión aparece en el input del programa escrito por el lex, se ejecuta el fragmento correspondiente. El usuario proporciona el código adicional necesario para completar estas funciones, incluyendo código escrito por otros generadores. El programa que reconoce las expresiones se genera en forma de fragmentos de programa C del usuario, El lex no es un lenguaje completo sino un generador que representa una cualidad de un nuevo lenguaje que se añade al leguaje de programación C. El lex convierte las expresiones y acciones del usuario (llamadas fuente en este capítulo) en un programa C llamado yylex. El programa yylex reconoce expresiones en un flujo (llamado input en este capítulo) y lleva a cabo las acciones especificadas para cada expresión a medida que se va detectando. Considere un programa para borrar del input todos los espacios en blanco y todos los tabuladores de los extremos de las líneas. Las líneas siguientes: %% [b\ t]+ $ ; es todo lo que se requiere. El programa contiene un delimitado %% para marcar el principio de las órdenes, y una orden. Esta orden contiene una expresión que coincide con una o más apariciones de los caracteres espacio en blanco o tabulador (escrito \ t para que se vea con mayor claridad, de acuerdo con la convención del lenguaje C) justo antes del final de una línea. Los corchetes indican la clase del carácter compuesto de espacios en blanco y tabuladores; el + indica uno o más del item anterior; y el signo de dólar ($) indica el final de la línea. No se especifica ninguna acción, por lo tanto el programa generado por el lex ignorará estos caracteres. Todo lo demás se copiará. Para cambiar cualquier cadena de caracteres en blanco o tabuladores que queden en un solo espacio en blanco, añada otra orden: %% [b\ t]+$ ; [b\ t] + printf (“ ”); La automatización generada por este fuente explora ambas órdenes a la vez, observa la terminación de la cadena de espacios o tabuladores haya o no un carácter newline, y después ejecuta la acción de la orden deseada. La primera orden coincide con todas las cadenas de caracteres de espacios en blanco o tabuladores hasta el final de las líneas, y la segunda orden coincide con todos los literales restantes de espacios o tabuladores. El lex se puede usar sólo para transformaciones sencillas, o por análisis o estadísticas buscando en un nivel léxico. El lex también se puede usar con un generador reconocedor para llevar a cabo la fase de análisis léxico; es especialmente fácil hacer que el lex y el yacc funcionen juntos. Los programas lex reconocen sólo expresiones regulares; yacc escribe reconocedores que aceptan una amplia clase de gramáticas de texto libre, pero que requieren un analizador de nivel bajo para reconocer tokens de input. Por lo tanto, a menudo es conveniente una combinación del lex y del yacc. Cuando se usa como un preprocesador para un generador, el lex se usa para dividir el input, y el generador de reconocimiento asigna una estructura a las piezas resultantes. - 16 - Los programas adicionales, escritos por otros generadores o a mano, se pueden añadir fácilmente a programas que han sido escritos por el lex. Los usuarios del yacc se darán cuenta de que el nombre yylex es el que el yacc da a su analizador léxico, de forma que el uso de este nombre por el lex simplifica el interface. El lex genera un autómata finito partiendo de expresiones regulares del fuente. El autómata es interpretado, en vez de compilado, para ahorrar espacio. El resultado es todavía un analizador rápido. En particular, el tiempo que utiliza un programa lex para reconocer y dividir una cadena de input es proporcional a la longitud del input. El número de órdenes lex o la complejidad de las órdenes no es importante para determinar la velocidad, a no ser que las órdenes que incluyan contexto posterior requieran una cantidad importante de exploración. Lo que aumenta con el número y complejidad de las órdenes es el tamaño del autómata finito, y por lo tanto el tamaño del programa generado por el lex. En el programa escrito por el lex, los fragmentos del usuario (representando acciones que se van a llevar a cabo a medida que se encuentra cada expresión) se colectan como casos de un intercambio. El intérprete del autómata dirige el flujo de control. Se proporciona la oportunidad al usuario para insertar declaraciones o sentencias adicionales en la rutina que contiene las acciones, o para añadir subrutinas fuera de esta rutina de acción. El lex no está limitado a fuente que se puede interpretar sobre la base de un posible carácter. Por ejemplo, si hay dos órdenes una que busca ab y la otra que busca abcdefg, y la cadena de caracteres del input es abcdefh, el lex reconocerá ab y dejará el puntero del input justo delante de cd. Tal precaución es más costosa que el proceso de lenguajes más sencillos. Formato fuente del lex El formato general de la fuente lex es: {definiciones} %% {órdenes} %% {subrutinas del usuario} donde las definiciones y las subrutinas del usuarios se omiten a menudo. El segundo %% es opcional, pero el primero se requiere para marcar el principio de las órdenes. El programa lex mínimo absoluto es por lo tanto %% (sin definiciones, ni órdenes) lo cual se traduce en un programa que copia el input en el output sin variar. En el formato del programa lex que se mostró anteriormente, las órdenes representan las decisiones de control del usuario. Estas forman una tabla en la cual la columna izquierda contiene expresiones regulares y la columna derecha contiene decisiones, fragmentos de programas que se ejecutarán cuando se reconozcan las expresiones. Por lo tanto la siguiente orden individual puede aparecer: integer printf(“ localizada palabra reservada INT ”) ; Esto busca el literal “integer” en el input e imprime el mensaje: localizada palabra reservada INT siempre que aparezca en el texto de input. En este ejemplo la función de librería printf() se usa para imprimir la cadena de caracteres o literal. El final de la expresión regular lex se indica por medio del primer carácter espacio en blanco o tabulador. Si la acción es simplemente una sola expresión C, se puede especificar en el lado derecho de la línea; si es compuesta u ocupa más de una línea, deberá incluirse entre llaves. Como ejemplo un poco más útil, suponga que se desea cambiar un número de palabras de la ortografía Británica a la Americana. Las órdenes lex tales como: - 17 - colour printf(“color”); mechanise printf(“mechanize”); petrol printf(“gas”); será una forma de empezar. Estas órdenes no son suficientes puesto que la palabra petroleum se convertirá en gaseum; una forma de proceder con tales problemas se describe en una sección posterior. - 18 -