Lexical Analysis (Scanning)
Transcription
Lexical Analysis (Scanning)
Lexical Analysis (Scanning) José Miguel Rivero [email protected] Barcelona School of Informatics (FIB) Technical University of Catalonia (UPC) José Miguel Rivero Lexical Analysis – p. 1/46 Lexical Analysis. Summary 2.1 El análisis léxico en la compilación 2.2 Motivación 2.3 Descripción del problema 2.4 Construcción de Thompson de AFN(er) a partir de una er 2.5 Algoritmo para decidir si AFN(er) reconoce w 2.6 Alternativa: calcular el AFD(er) para AFN(er) 2.7 Comparación de los dos métodos 2.8 Algoritmo de análisis léxico 2.9 Tratamiento de errores léxicos 2.10 Generación automática de analizadores léxicos: ANTLR, flex, . . . José Miguel Rivero Lexical Analysis – p. 2/46 2.1 Análisis léxico en la compilación Estructura conceptual vs. estructura real Objetivo. Tokens Otros componentes léxicos Atributos de los tokens Ejemplo José Miguel Rivero Lexical Analysis – p. 3/46 Estructura conceptual vs. estructura real Estructura conceptual programa fuente ANALISIS LEXICO lista de tokens ANALISIS SINTACTICO arbol sintactico ANALISIS SEMANTICO arbol sintactico decorado Estructura procedural (real) programa fuente token ANALISIS LEXICO ANALISIS SINTACTICO arbol sintactico ANALISIS SEMANTICO arbol sintactico decorado necesito token 1ª pasada 2ª pasada representaciones internas José Miguel Rivero Lexical Analysis – p. 4/46 Objetivo. Tokens Objetivo: descomponer la secuencia de caracteres del programa fuente en una secuencia de componentes léxicos (tokens) ¿Cúales son los tokens que debemos reconocer y enviar al analizador sintáctico?: palabras clave del lenguaje (while, endvars, write, . . .) operadores (+, /, “<=”, OR, “:=”, . . .) otros símbolos del lenguaje (paréntesis, coma, punto y coma, etc) identificadores (numels), valores enteros (834), cadenas de caracteres ("Hello world!"), reales (3.04E-3), . . . ... José Miguel Rivero Lexical Analysis – p. 5/46 Otros componentes léxicos Existen componentes léxicos que deben reconocerse aunque no tengan interés para etapas posteriores del compilador: separadores: espacios, tabuladores, . . . comentarios: /* ... */ en C, // ... en C++ cambios de línea. Interesa conocer en que línea encontramos los tokens para situar los errores de compilación Atributos de los tokens en todos: el número de línea en identificadores, constantes numéricas, strings, . . . : el texto del token (el prefijo reconocido) José Miguel Rivero Lexical Analysis – p. 6/46 Ejemplo Programa fuente: Program Vars Integer i Real r EndVars 1: Program 2: Vars 3: Integer i 4: Real r EndVars 5: 6: 7: i := 1 ; r := 1.17 i := 1 ; r := 1.17 While i < 25 Do // 24 vueltas While i < 25 Do // 24 vueltas 8: 9: r := r / i ; i := i + 1 r := r / i ; i := i + 1 EndWhile EndWhile 10: Write ( "fin" ) Write ( "fin" ) 11: 12: EndProgram EndProgram Secuencia de tokens: PROGRAM VARS INTEGER IDENT("i") REAL IDENT("r") ENDVARS IDENT("i") ASIG INTCONST("1") PUNTCOM IDENT("r") ASIG REALCONST("3.14") WHILE IDENT("i") MENOR INTCONST("24") DO IDENT("r") ASIG IDENT("r") DIVREAL IDENT("i") PUNTCOM IDENT("i") ASIG IDENT("i") MAS INTCONST("1") ENDWHILE WRITE PARABR STRINGCONST("fin") PARCERR ENDPROGRAM José Miguel Rivero Lexical Analysis – p. 7/46 2.2 Motivación ¿Por qué definir una etapa específica que realiza el análisis léxico de un programa?: Conceptualmente es una tarea diferenciada: filtra la entrada y la descompone en aquellos elementos que tienen interés para la siguiente etapa, el análisis sintáctico. Las técnicas que veremos serán sencillas y eficientes (no debemos matar moscas a cañonazos) flexibles (los cambios a nivel léxico se pueden resolver fácilmente) transportables y generales Estas técnicas tienen otras muchas aplicaciones. José Miguel Rivero Lexical Analysis – p. 8/46 Otras aplicaciones Recuperación de información (análisis de queries) Problemas de genética Editores de texto (editores dirigidos por la sintaxis, . . . ) Sistemas operativos (lenguajes de commandos, grep) Ejemplo (en unix): % rm prog*.[ch] Lenguajes de programación (del tipo patrón/acción : (AWK ) Verificación de circuitos ... José Miguel Rivero Lexical Analysis – p. 9/46 2.3 Descripción del problema Expresiones regulares Poder expresivo de las expresiones regulares Problema a resolver por el analizador léxico Ejemplos Criterio para deshacer ambigüedades ¡Cuidado al definir el lenguaje! José Miguel Rivero Lexical Analysis – p. 10/46 Expresiones regulares Los elementos léxicos de un lenguaje de programación se especifican con expresiones regulares sobre un cierto alfabeto Σ. Reglas de formación: er = ǫ es una expresión regular er = a es una expresión regular para todo a ∈ Σ si er1 y er2 son expresiones regulares, er = er1 | er2 es una expresión regular si er1 y er2 son expresiones regulares, er = er1 er2 es una expresión regular si er1 es una expresión regular, er = er1∗ es una expresión regular si er1 es una expresión regular, er = (er1 ) es una expresión regular José Miguel Rivero Lexical Analysis – p. 11/46 Poder expresivo de las er’s Mediante expresiones regulares no podemos reconocer, por ejemplo, expresiones bien parentizadas por ejemplo del tipo {an bn }: los autómatas finitos no saben contar. Tampoco podemos reconocer las palabras del lenguaje {nan : n ≥ 0} = { 0, 1a, 2aa, 3aaa, . . . } Strings repetidos no pueden ser especificados con expresiones regulares. El conjunto { w c w | w ∈ (a|b)∗ } no es un lenguaje regular, ni tampoco puede ser descrito con gramáticas incontextuales. José Miguel Rivero Lexical Analysis – p. 12/46 Problema a resolver en el scanning Tenemos una lista de expresiones regulares er1 , . . . , ern que describen los componentes léxicos del lenguaje, y un programa fuente a descomponer (la palabra w). Dadas las expresiones regulares er1 , . . . , ern y la palabra w, se trata de encontrar el prefijo más largo v de w t.q. v ∈ L(eri ) para alguna i. En caso de encontrar más de una expresión regular para el prefijo v , se elegirá la eri con la i mínima. La siguiente llamada al analizador léxico vuelve a hacer lo mismo con el resto de la entrada por tratar (si w = v w′ después buscará el prefijo más largo de w′ ) y así hasta agotar la entrada. José Miguel Rivero Lexical Analysis – p. 13/46 Ejemplos Se trata de encontrar el prefijo más largo, por tanto si tenemos "programacion ..." no nos dirá que tenemos la palabra clave program seguido del identificador acion si tenemos "while ..." no nos dirá que tenemos el identificador "while" si tenemos "ab24.8 ..." no nos dirá que tenemos el identificador "ab" seguido del real "24.8" (a menos que los identificadores sólo contengan caracteres alfabéticos) José Miguel Rivero Lexical Analysis – p. 14/46 Ejemplos (cont.) Si tenemos "10..20 ..." nos dirá que tenemos el entero "10" porque aunque intenta reconocer un real que empieza por "10." cuando ve el segundo ’.’ se da cuenta que no puede y tiene que volver al lugar en el que ya había reconocido algo. Después reconocerá los dos puntos y por último el segundo entero. ¡Ojo, no linealidad! Por tanto, se tiene que recordar el punto en que acabó el último prefijo aceptado (y por qué eri ) por si no podemos encontrar uno aún más largo. Si en cambio sí encontramos un prefijo aún más largo nos olvidamos del anterior. Para no perder linealidad podemos exigir que al buscar un prefijo aún más largo, sólo podemos atravesar estados aceptadores. José Miguel Rivero Lexical Analysis – p. 15/46 Deshacer ambigüedades léxicas Se tiene que reconocer el prefijo más largo posible Se tienen que especificar primero (menor i) las expresiones regulares que corresponden a palabras clave, que la que define los identificadores: una palabra clave forma parte del lenguaje de las dos, pero la tenemos que reconocer como palabra clave y no como identificador. Ejemplo de analizador léxico en flex: program vars {...; return(PROGRAM) } {...; return(VARS) } ... "," {...; return(COMA) } ... [0-9]+ [A-Za-z][A-Za-z0-9]* ... {...; return(ENTERO) } {...; return(IDENT) } José Miguel Rivero Lexical Analysis – p. 16/46 ¡Cuidado al definir el lenguaje! Es importante hacer una definición muy meditada tanto de los componentes léxicos como de la estructura sintáctica de un lenguaje. Por ejemplo, estas son algunas de las situaciones que se podrían dar: en Fortran, la expresión DO 5 I = 1,25 ... representa el comienzo de un bucle. Si en lugar de 1,25 hubiéramos escrito 1.25 estaríamos asignando dicho valor real a la variable DO5I: no free-format si obligamos a que las etiquetas (pasa también en Fortran) estén situadas en la primera columna complicaremos el análisis léxico si permitimos que los reales puedan tener parte decimal vacía, el rango (de un array ) 10..40 no se analizará correctamente José Miguel Rivero Lexical Analysis – p. 17/46 2.4 Constr. de Thompson de AFN(er) Construcción del autómata N (er) para las expresiones regulares ǫ, a, er1 |er2 , er1 er2 , er1∗ y (er1 ), donde er1 y er2 son expresiones regulares con autómatas ya construidos N (er1 ) y N (er2 ). er = λ er = a λ er = er1 | er2 a λ N(er1) λ λ λ N(er2) er = er1 er2 N(er1) er = er1* N(er2) λ λ N(er1) er = (er1) λ N(er) N(er1) λ José Miguel Rivero Lexical Analysis – p. 18/46 Construcción de Thompson (cont.) Invariante de la construcción: todos los AFN’s tienen un estado inicial sin aristas de entrada, y un solo estado final sin aristas de salida El número de estados del AFN(er) ≤ 2|er|, porque se añaden como máximo 2 nuevos estados por cada paso durante la construcción Tenemos como máximo 2 aristas salientes (2 transiciones) por cada estado del autómata. Por tanto, podemos obtener una representación compacta del mismo. José Miguel Rivero Lexical Analysis – p. 19/46 Ejemplo 1 Autómata finito indeterminista para la expresión regular er = (a|b)∗ abb. Estos son los primeros pasos de las construcción del AFN: er = b er = a er = a | b b a a λ λ λ er = (a | b)* er = (a | b)* a λ λ a λ λ λ a λ b λ λ b λ λ λ λ λ λ b λ a λ λ José Miguel Rivero Lexical Analysis – p. 20/46 Ejemplo 2 Combinación de AFN’s en la disyunción de las eri ’s para resolver el problema del análisis léxico N(er1) f1 λ N(er2) f2 λ . . . λ er = er1 | er2 | ... | ern i N(ern) fn José Miguel Rivero Lexical Analysis – p. 21/46 2.5 Algoritmo de decision para AFN(er) Definimos en primer lugar dos funciones auxiliares: ǫ-clausura(S) es el conjunto de estados accesibles desde los estados de S a través de cero o más ǫ-transiciones. move(S, a) es el conjunto de estados accesibles desde los estados de S con una transición etiquetada con a. Algoritmo que decide si AFN(er) reconoce w: P re : s0 es el estado inicial del autómata AF N F es el conjunto de estados finales de AF N eof es el símbolo con el que acaba w S := ǫ-clausura({s0 }); a := LeerSimbolo( ); while a ! = eof do S := ǫ-clausura(move(S, a)); a := LeerSimbolo( ); endwhile P ost : AF N acepta w ssi S ∩ F 6= ∅ José Miguel Rivero Lexical Analysis – p. 22/46 Análisis del algoritmo Coste temporal del algoritmo: O(|er| · |w|) Coste espacial (tamaño de la tabla de transiciones del autómata): O(|er|) Pero recordemos que el objetivo del análisis léxico en la compilación es obtener el prefijo más largo de w y dar prioridad a la i mínima en caso de “empate”. Para ello hay que modificar ligeramente el algoritmo anterior: tenemos que mantener el estado de aceptación más reciente (si hay varios sólo el de la i mínima), y además guardar la longitud del prefijo correspondiente. Así cuando ya no haya transición posible, sabremos cúal fue el último prefijo aceptado y por qué expresión regular. José Miguel Rivero Lexical Analysis – p. 23/46 2.6 Alternativa: calcular el AFD(er) Algoritmo de determinización a partir de AFN(er) Ejemplo Coste espacial del ADF Algoritmo de minimización de AFD’s Ejemplo Algoritmo de aceptación de w por el AFD Técnicas de compresión José Miguel Rivero Lexical Analysis – p. 24/46 Algoritmo de determinización Autómata determinista: desde ningún estado hay ǫ-transiciones ni tampoco más de una arista de salida para un mismo símbolo a ∈ Σ. Técnica de cálculo de subconjuntos. Entenderemos cada subconjunto final de estados como un estado del autómata determinista y calcularemos las transiciones entre estos estados. Algoritmo: construiremos Dstate (el conjunto de estados de AFD) y Dtran (la tabla de transiciones del AFD). Los estados en Dstate se marcan cuando se calculan sus transiciones en Dtran. José Miguel Rivero Lexical Analysis – p. 25/46 Algoritmo de determinización (cont.) Pre: s0 es el estado inicial del autómata AF N F es el conjunto de estados finales de AF N ǫ-clausura({s0 }) es el único estado en Dstate y no está marcado while exista un estado S sin marcar en Dstate do marca S foreach simbolo de entrada a ∈ Σ do S ′ := ǫ-clausura(move(S, a)); if S ′ ∈ / Dstate then añade S ′ (sin marcar) a Dstate endif Dtran[S, a] := S ′ ; endfor endwhile Post: el estado inicial de AF D es ǫ-clausura({s0 }) Son estados finales de AF D todos aquellos (conjuntos de) estados que contengan al menos un estado de F José Miguel Rivero Lexical Analysis – p. 26/46 Ejemplo Calcular el autómata determinista para la expresión regular er = (a|b)∗ abb NFA: 0 λ λ λ a 2 3 λ 1 6 λ b 4 λ 7 a 8 b 9 b 10 λ 5 λ ǫ-clausura({0}) = ǫ-clausura(move(A, a)) {0, 1, 2, 4, 7} = A = ǫ-clausura({3, 8}) = {1, 2, 3, 4, 6, 7, 8} = B = ǫ-clausura({5}) = {1, 2, 4, 5, 6, 7} = C Dtran[A, a] = B ǫ-clausura(move(A, b)) Dtran[A, b] = C ... José Miguel Rivero Lexical Analysis – p. 27/46 Ejemplo (cont.) Dtran: símbolo estado a b A B C B B D C B C D B E E B C DFA: b C b b a A a B b b D E a a a José Miguel Rivero Lexical Analysis – p. 28/46 Coste espacial del AFD El coste en espacio (número de estados de la tabla Dtran) puede ser exponencial respecto de la longitud de er (el número de subconjuntos distintos de un conjunto de N elementos es 2N ) Ejemplo: Dada la expresión regular (a|b)∗ a(a|b)k construiremos un AFN de la siguiente forma: Un estado inicial 0 con aristas etiquetadas con a y b hacia sí mismo, y una arista etiquetada con a hacia el estado 1 Transiciones desde el estado i etiquetadas con a y b hacia el estado i+1, para i ∈ [1..k] El estado k +1 es el estado final José Miguel Rivero Lexical Analysis – p. 29/46 Coste espacial del AFD (cont.) a, b 0 a 1 a, b ... a, b k a, b k+1 El tamaño del AFD correspondiente es exponencial porque necesita recordar k + 1 bits (los últimos k + 1 símbolos leídos) Con k = 3: abba (estado final) −→a bbaa (estado no final) baba (estado no final) −→b abab (estado final) José Miguel Rivero Lexical Analysis – p. 30/46 Algoritmo de minimización de AFD’s Calcula particiones sucesivas del conjunto de estados. P re : S es el conjunto de estados de AF D s0 es el estado inicial de AF D F es el conjunto de estados finales de AF D P ost : AF D′ acepta el mismo lenguaje que AF D teniendo el mínimo número de estados posible José Miguel Rivero Lexical Analysis – p. 31/46 Algoritmo de minimización (cont.) Calcula particiones sucesivas del conjunto de estados. partición inicial Π = Πnew con dos grupos : estados finales F y estados no finales S \ F repeat Π := Πnew f or each grupo G de Π do 1. divide G en subgrupos t.q. dos estados s y t de G quedan en el mismo subgrupo ssi para todo símbolo a ∈ Σ, s y t tienen transiciones hacia estados en el mismo grupo de Π. 2. reemplaza G en Πnew por el conjunto de subgrupos formados endf or until Πnew = Π José Miguel Rivero Lexical Analysis – p. 32/46 Algoritmo de minimización (cont.) Se construye AF D′ : 1. Sus estados se definen eligiendo un representante de cada grupo 2. Las transiciones en AF D′ serán las que existen entre los estados representantes de AF D 3. El estado inicial de AF D′ será el representante del grupo que contiene s0 4. Los estados finales serán aquellos que tengan representantes en F José Miguel Rivero Lexical Analysis – p. 33/46 Ejemplo Minimización del autómata determinista que reconoce (a|b)∗ abb DFA: b C b b a A a B b b D E a a a Comentario Particiones estados no finales / finales (ABCD) (E) A, B, C →b (ABCD) pero D →b (E) (ABC) (D) (E) (D) (E) A, C →b (ABC) pero B →b (D) (AC) (B) partición final José Miguel Rivero Lexical Analysis – p. 34/46 Ejemplo (cont.) Dtran: símbolo estado a b AC B AC B B D D B E E B AC DFAmin : b b AC a B b b D E a a a José Miguel Rivero Lexical Analysis – p. 35/46 Otro forma de calcular AFD(er) Evita determinizar el AFN(er) y aplicar después el algoritmo de minimización. Realiza estos dos pasos en uno. No siempre obtiene el AFD(er) mínimo pero es una buena técnica en la mayoria de los casos José Miguel Rivero Lexical Analysis – p. 36/46 Algoritmo de decision para AFD(er) P re : s0 es el estado inicial del autómata AF D F es el conjunto de estados finales de AF D eof es el símbolo con el que acaba w s := s0 ; a := LeerSimbolo( ); while a ! = eof do s := Dtran[s, a]; a := LeerSimbolo( ); endwhile P ost : AF D acepta w ssi s ∈ F Tiene un coste temporal lineal en la longitud de la entrada O(|w|) Tiene un coste espacial (tamaño de Dtran) O( (número de estados del AFD) ∗ (número de símbolos de Σ) ) = O(2|er| ) José Miguel Rivero Lexical Analysis – p. 37/46 Técnicas de compresión Existen diferentes formas de implementar la función de transición de un AF D. La más directa es usando una tabla de transición. El tamaño de esa tabla depende del número de estados del autómata y del número de símbolos del alfabeto. Dado que: 1. el número de estados puede ser bastante elevado y 2. no existen transiciones desde cada estado para todos los símbolos (más bien al contrario, o bien es al mismo estado para casi todos los símbolos) esta enorme tabla se encuentra mayormente vacía (sparse) e interesa trabajar con una representación más compacta. José Miguel Rivero Lexical Analysis – p. 38/46 Técnicas de compresión (cont.) Vector unidimensional de estados. Desde cada estado cuelga la lista de transiciones que sí están definidas desde él, más eventualmente la transición que se hace por defecto (o en caso de error). Es la más sencilla pero el tiempo de cálculo del nuevo estado puede empeorar sensiblemente. Otras técnicas intentan aprovechar (a groso modo) las casillas vacías contiguas que había en la tabla inicial antes del primero y a partir del último símbolo con transición en cada estado. En esos métodos se utilizan varias tablas para saber, por ejemplo, donde comienza la fila de transiciones de un estado. Así se mejora mucho el tiempo para calcular una transición comparado con la técnica anterior y el espacio se aprovecha bastante mejor que utilizando la tabla inicial. José Miguel Rivero Lexical Analysis – p. 39/46 Técnicas de compresión (cont.) Podemos también solapar dos o más filas siempre que las transiciones definidas en una y en otra no coincidan. Esta técnica se muestra en la siguiente figura: if check[base[s] + a] = s then next(s, a) = next[base[s] + a] else next(s, a) = error base +a s +b s’ next check ... t s s s’ s tran(s, a) = t s’ s’ s’ ... ... s s’ s’ ... tran(s’, b) = error José Miguel Rivero Lexical Analysis – p. 40/46 2.7 Comparación de los dos métodos Resumen de costes: Autómata Coste temporal Coste espacial AF N AF D O(|er| · |w|) O(|w|) O(|er|) O(2|er| ) En general, cuando los dos métodos son viables (el coste en espacio del AFD no es excesivamente mayor) podemos concluir que: AF N es adecuado cuando |er| ↓↓ AF D es adecuado cuando |w| ↑↑ o |er| ↑↑ José Miguel Rivero Lexical Analysis – p. 41/46 Cálculo perezoso (lazy) de transiciones Combina las necesidades de espacio del autómata AFN con las ventajas en tiempo del autómata AFD. Se trabaja a partir del autómata indeterminista, calculando sólo los subconjuntos de estados que se van necesitando. Estos subconjuntos (y sus transiciones) se guardan en una cache de forma que no hace falta volver a calcularlos de nuevo. Resumiendo sus ventajas: Menor necesidad de espacio: tamaño de la tabla del AFN ( O(|er|) ) + tamaño de la cache No se calculan transiciones entre estados que nunca son utilizadas. Esto lo hace casi tan rápido como los AFD José Miguel Rivero Lexical Analysis – p. 42/46 2.8 Algoritmo de análisis léxico Ejercicio: suponiendo que los carácteres de la entrada w (contando eof ) se encuentran en un array A indexado desde 0, se trata de modificar el algoritmo anterior para que reconozca el prefijo más largo v = A[0] · · · A[k − 1] que encaja con alguna de las expresiones regulares eri . Cuando la tabla Dtran no contiene transición para un cierto estado s y carácter a entonces contiene el valor -1. Para distinguir un estado final de uno no final tenemos otro array DF in que dado un estado s contiene true si s es un estado final y false en caso contrario. El algoritmo tiene que devolver el índice k y el estado (final) s tal que: la palabra A[0] · · · A[k − 1] es el prefijo más largo que encaja con alguna eri . Si no existe tal prefijo devuelve -1. en ese momento el autómata se encuentra en estado s (que es el estado final de la expresión regular eri correspondiente) José Miguel Rivero Lexical Analysis – p. 43/46 Algoritmo de análisis léxico (cont.) p := f := 1; l := 0; ∀i : 1 ≤ i ≤ n : qi := Inii ; // Estados iniciales while p ≤ m do ∀i : 1 ≤ i ≤ n : qi := δi ( qi , IN[p] ); p :=p + 1; // Transición de estados if ∃ i : 1 ≤ i ≤ n : qi ∈ Fi then // Estado f inal l := p − 1; a = smallest i su ch that qi ∈ Fi ; elseif ∀ i : 1 ≤ i ≤ n : qi ∈ Erri then // Estados pozo if l ≥ f then Generate token of type a with word ; IN[f.. l] p := f := l + 1; l := f − 1; ∀i : 1 ≤ i ≤ n : qi := Inii ; else Lexical error endif endif endwhile if l < f then Lexical error endif José Miguel Rivero Lexical Analysis – p. 44/46 2.9 Tratamiento de errores léxicos ¿Cuándo se produce un error léxico? Recordemos que el analizador léxico intentar obtener el prefijo v más largo de la palabra w que está analizando. Ese prefijo tiene que formar parte del lenguaje de alguna de las expresiones regulares er1 , er2 , . . . , ern . En este proceso se puede llegar a un estado s desde el cual no haya transición definida para el símbolo en curso a. Esto le obligaría a devolver el prefijo anterior más largo que ya había reconocido. ¿Qué ocurre si todavía no había podido reconocer ningún prefijo que encajara en alguna expresión regular antes de intentar buscar otro aún más largo? Pues que la seqüencia que comienza con el primer carácter de w no forma parte del lenguaje de ninguna de las eri : ⇒ se produce un error léxico. José Miguel Rivero Lexical Analysis – p. 45/46 Tratamiento de errores léxicos (cont.) Hay diferentes formas de tratar estas situaciones de error. Estas son algunas de ellas: En modo pánico. Se ignora el primer carácter de w (y sucesivos si también es necesario) hasta que podamos reconocer algún prefijo en la entrada Sólo se borran caracteres extraños. Por ejemplo, aquellos que no forman parte del alfabeto del lenguaje: ’¿’, ’ç’, ’@’, . . . en lenguajes como C o Pascal Se realizan correcciones: insertar un carácter, reemplazar un carácter por otro distinto, intercambiar adyacentes (wihle pasa a ser while), etc. En general se intenta que el número de correciones sea el mínimo necesario. No es una técnica muy frecuente en la práctica José Miguel Rivero Lexical Analysis – p. 46/46