resueltos programacion ejemplos completo comandos javascript lexer

programacion - manual javascript pdf 2018



Al analizar Javascript, ¿qué determina el significado de una barra? (5)

Javascript tiene una gramática difícil de analizar. Las barras diagonales hacia delante pueden significar varias cosas diferentes: operador de división, literal de expresión regular, introductor de comentarios o introductor de comentarios de línea. Los dos últimos son fáciles de distinguir: si la barra es seguida por una estrella, comienza un comentario multilínea. Si la barra es seguida por otra barra, es un comentario de línea.

Pero las reglas para desambiguar la división y las expresiones regulares se me están escapando. No lo encuentro en el estándar ECMAScript . Allí, la gramática léxica se divide explícitamente en dos partes, InputElementDiv y InputElementRegExp, dependiendo de lo que significará una barra. Pero no hay nada que explique cuándo usar cuál.

Y, por supuesto, las temidas reglas de inserción en punto y coma complican todo.

¿Alguien tiene un ejemplo de código claro para el Javascript Javascript que tenga la respuesta?


Actualmente estoy desarrollando un analizador JavaScript / ECMAScript 5.1 con JavaCC. RegularExpressionLiteral y Automatic Semicolon Insertion son dos cosas que me vuelven loco en la gramática ECMAScript. Esta pregunta y las respuestas fueron invaluables para la pregunta de expresiones regulares. En esta respuesta me gustaría juntar mis propias conclusiones.

TL; DR En JavaCC, use estados léxicos y cámbielos desde el analizador .

Muy importante es lo que Thom Blake escribió:

El operador de división debe seguir una expresión, y un literal de expresión regular no puede seguir una expresión, por lo que en todos los demás casos puede asumir que está mirando un literal de expresión regular.

Entonces realmente necesitas entender si era una expresión o no antes . Esto es trivial en el analizador pero muy duro en el lexer.

Como señaló Thom, en muchos casos (pero, desafortunadamente, no en todos), se puede entender si se trataba de una expresión "mirando" el último token. Hay que considerar los puntuadores, así como las palabras clave.

Vamos a empezar con las palabras clave. Las siguientes palabras clave no pueden preceder a un DivPunctuator (por ejemplo, no puede tener case /5 ), por lo que si ve una / después de estas, tiene un RegularExpressionLiteral :

case delete do else in instanceof new return throw typeof void

A continuación, los puntuadores. Los siguientes puntuadores no pueden preceder a un DivPunctuator (por ejemplo, en { /a... el símbolo / nunca puede iniciar una división):

{ ( [ . ; , < > <= >= == != === !== + - * % << >> >>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>= &= |= ^= /=

Entonces, si tienes uno de estos y ves /... después de esto, esto nunca puede ser un DivPunctuator y por lo tanto debe ser un RegularExpressionLiteral .

A continuación, si tienes:

/

Y /... después de eso también debe ser un RegularExpressionLiteral . Si no hubiera espacio entre estas barras (es decir, // ... ), esto debe haberse manejado como un SingleLineComment ("maximal munch").

A continuación, el siguiente puntuador solo puede terminar una expresión:

]

Así que lo siguiente / debe iniciar un DivPunctuator .

Ahora tenemos los siguientes casos restantes que, desafortunadamente, son ambiguos:

} ) ++ --

Para } y ) debe saber si terminan una expresión o no, para ++ y -- - terminan una PostfixExpression o inician una UnaryExpression .

Y he llegado a la conclusión de que es muy difícil (si no imposible) descubrirlo en el lexer. Para darte una idea de eso, un par de ejemplos.

En este ejemplo:

{}/a/g

/a/g es un RegularExpressionLiteral , pero en este:

+{}/a/g

/a/g es una división.

En el caso de ) puedes tener una división:

(''a'')/a/g

así como un RegularExpressionLiteral :

if (''a'')/a/g

Entonces, desafortunadamente, parece que no se puede resolver solo con el lexer. O tendrás que introducir tanta gramática en el lexer para que ya no sea más el lexer.

Esto es un problema.

Ahora, una posible solución, que es, en mi caso, basada en JavaCC.

No estoy seguro de si tiene características similares en otros generadores de analizadores, pero JavaCC tiene una característica de estados léxicos que se puede usar para cambiar entre los DivPunctuator "esperamos un DivPunctuator " y "esperamos un RegularExpressionLiteral ". Por ejemplo, en esta gramática el estado NOREGEXP significa "no esperamos un RegularExpressionLiteral aquí".

Esto resuelve parte del problema, pero no el ambiguo ) , } , ++ y -- .

Para ello, deberá poder cambiar los estados léxicos desde el analizador. Esto es posible, vea la siguiente pregunta en Preguntas frecuentes de JavaCC :

¿Puede el analizador forzar un cambio a un nuevo estado léxico?

Sí, pero es muy fácil crear errores al hacerlo.

Es posible que un analizador lookahead ya haya ido demasiado lejos en la secuencia del token (es decir, ya leído / como DIV o viceversa).

Afortunadamente, parece que hay una manera de hacer que el cambio de estados léxicos sea un poco más seguro:

¿Hay alguna manera de hacer que SwitchTo sea más seguro?

La idea es hacer una secuencia de token de "copia de seguridad" y empujar tokens leídos durante la búsqueda hacia atrás de nuevo.

Creo que esto debería funcionar para } , ) , ++ , -- ya que normalmente se encuentran en situaciones LOOKAHEAD (1), pero no estoy 100% seguro de eso. En el peor de los casos, es posible que el lexer ya haya intentado analizar / token de inicio como un RegularExpressionLiteral y haya fallado porque no fue terminado por otro / .

En cualquier caso, no veo mejor manera de hacerlo. Lo siguiente bueno sería probablemente abandonar el caso por completo (como lo JSLint y muchos otros), documentar y simplemente no analizar este tipo de expresiones. {}/a/g no tiene mucho sentido de todos modos.


En realidad es bastante fácil, pero requiere que tu lexer sea un poco más inteligente de lo habitual.

El operador de división debe seguir una expresión, y un literal de expresión regular no puede seguir una expresión, por lo que en todos los demás casos puede asumir que está mirando un literal de expresión regular.

Ya tiene que identificar los Puntuadores como cadenas de caracteres múltiples, si lo está haciendo bien. Así que mire el token anterior y vea si es alguno de estos:

. ( , { } [ ; , < > <= >= == != === !== + - * % ++ -- << >> >>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>= &= |= ^= / /=

Para la mayoría de estos, ahora sabe que está en un contexto donde puede encontrar un literal de expresión regular. Ahora, en el caso de ++ -- , necesitarás hacer un trabajo extra. Si el ++ o -- es un incremento previo / decremento, entonces / sigue un literal de expresión regular; Si se trata de un incremento / decremento posterior, el / siguiente inicia un DivPunctuator.

Afortunadamente, puede determinar si se trata de un operador "pre-" comprobando su token anterior. Primero, post-incremento / decremento es una producción restringida, por lo que si ++ o -- está precedido por un salto de línea, entonces usted sabe que es "pre-". De lo contrario, si el token anterior es cualquiera de las cosas que pueden preceder a un literal de expresión regular (¡yay recursión!), Entonces sabe que es "pre-". En todos los demás casos, es "post-".

Por supuesto, el puntuador no siempre indica el final de una expresión, por ejemplo if (something) /regex/.exec(x) . Esto es complicado porque requiere cierta comprensión semántica para desenmarañar.

Lamentablemente, eso no es todo. Hay algunos operadores que no son Puntuadores y otras palabras clave notables para arrancar. Los literales de expresión regular también pueden seguir estos. Son:

new delete void typeof instanceof in do return case throw else

Si el Nombre de identificador que acaba de consumir es uno de estos, entonces está buscando un literal de expresión regular; De lo contrario, es un divPunctuator.

Lo anterior se basa en la especificación ECMAScript 5.1 (como se encuentra here ) y no incluye ninguna extensión específica del navegador al idioma. Pero si necesita apoyarlos, entonces esto debería proporcionar pautas fáciles para determinar en qué tipo de contexto se encuentra.

Por supuesto, la mayoría de los anteriores representan casos muy tontos por incluir un literal de expresión regular. Por ejemplo, en realidad no puede preincrementar una expresión regular, aunque se permita de manera sintáctica. Así que la mayoría de las herramientas pueden simplificar la verificación del contexto de expresiones regulares para aplicaciones del mundo real. El método de JSLint para verificar el carácter anterior para (,=:[!&|?{}; Probablemente sea suficiente. Pero si toma ese atajo al desarrollar lo que se supone que es una herramienta para leer JS, debe asegurarse de tener en cuenta ese.


JSLint parece esperar una expresión regular si el token anterior es uno de

(,=:[!&|?{};

Rhino siempre devuelve un token DIV desde el lexer.



Ver sección 7:

Hay dos símbolos de gol para la gramática léxica. El símbolo InputElementDiv se usa en aquellos contextos de gramática sintáctica donde se permite un operador de división (/) o asignación de división (/ =). El símbolo InputElementRegExp se usa en otros contextos gramaticales sintácticos.

NOTA No hay contextos gramaticales sintácticos donde se permita tanto una división líder o una asignación de división, como un RegularExpressionLiteral líder. Esto no se ve afectado por la inserción de punto y coma (ver 7.9); En ejemplos como los siguientes:

a = b /hi/g.exec(c).map(d);

donde el primer carácter sin espacios en blanco, sin comentarios después de un LineTerminator es una barra (/) y el contexto sintáctico permite la división o asignación de división, no se inserta ningún punto y coma en el LineTerminator. Es decir, el ejemplo anterior se interpreta de la misma manera que:

a = b / hi / g.exec(c).map(d);

Estoy de acuerdo, es confuso y debería haber una expresión gramatical de nivel superior en lugar de dos.

editar:

Pero no hay nada que explique cuándo usar cuál.

Tal vez la respuesta simple es mirarnos a la cara: pruebe uno y luego pruebe el otro. Como ambos no están permitidos, a lo sumo uno dará una coincidencia sin errores.