Los analizadores normales, como generalmente se enseñan, tienen una etapa lexer antes de que el analizador toque la entrada. El lexer (también "escáner" o "tokenizador") corta la entrada en pequeños tokens que están anotados con un tipo. Esto permite que el analizador principal use tokens como elementos terminales en lugar de tener que tratar a cada personaje como un terminal, lo que conduce a ganancias de eficiencia notables. En particular, el lexer también puede eliminar todos los comentarios y espacios en blanco. Sin embargo, una fase de tokenizador separada significa que las palabras clave no se pueden usar también como identificadores (a menos que el idioma admita la eliminación de caracteres que ha caído en desuso o prefija todos los identificadores con un sigilo $foo).
¿Por qué? Supongamos que tenemos un tokenizador simple que comprende los siguientes tokens:
FOR = 'for'
LPAREN = '('
RPAREN = ')'
IN = 'in'
IDENT = /\w+/
COLON = ':'
SEMICOLON = ';'
El tokenizer siempre coincidirá con el token más largo y preferirá las palabras clave sobre los identificadores. Entonces interestingserá lexed como IDENT:interesting, pero inserá lexed como IN, nunca como IDENT:interesting. Un fragmento de código como
for(var in expression)
será traducido a la secuencia de tokens
FOR LPAREN IDENT:var IN IDENT:expression RPAREN
Hasta ahora, eso funciona. Pero cualquier variable insería lexed como la palabra clave en INlugar de una variable, lo que rompería el código. El lexer no mantiene ningún estado entre los tokens, y no puede saber que innormalmente debería ser una variable, excepto cuando estamos en un bucle for. Además, el siguiente código debe ser legal:
for(in in expression)
El primero insería un identificador, el segundo sería una palabra clave.
Hay dos reacciones a este problema:
Las palabras clave contextuales son confusas, reutilicemos las palabras clave en su lugar.
Java tiene muchas palabras reservadas, algunas de las cuales no tienen uso, excepto para proporcionar mensajes de error más útiles a los programadores que cambian a Java desde C ++. Agregar nuevas palabras clave rompe el código. Agregar palabras clave contextuales es confuso para un lector del código a menos que tenga un buen resaltado de sintaxis, y hace que las herramientas sean difíciles de implementar porque tendrán que usar técnicas de análisis más avanzadas (ver más abajo).
Cuando queremos extender el lenguaje, el único enfoque sensato es usar símbolos que anteriormente no eran legales en el idioma. En particular, estos no pueden ser identificadores. Con la sintaxis del bucle foreach, Java reutilizó la :palabra clave existente con un nuevo significado. Con lambdas, Java agregó una ->palabra clave que no podía aparecer previamente en ningún programa legal ( -->aún estaría lex como lo '--' '>'que es legal, y ->podría haber sido previamente lexed as '-', '>', pero esa secuencia sería rechazada por el analizador).
Las palabras clave contextuales simplifican los idiomas, impleméntelos
Los Lexers son indiscutiblemente útiles. Pero en lugar de ejecutar un lexer antes del analizador, podemos ejecutarlos en conjunto con el analizador. Los analizadores ascendentes siempre conocen el conjunto de tipos de tokens que serían aceptables en cualquier ubicación. El analizador puede solicitar al lexer que coincida con cualquiera de estos tipos en la posición actual. En un ciclo for-each, el analizador estaría en la posición indicada ·en la gramática (simplificada) después de que se haya encontrado la variable:
for_loop = for_loop_cstyle | for_each_loop
for_loop_cstyle = 'for' '(' declaration · ';' expression ';' expression ')'
for_each_loop = 'for' '(' declaration · 'in' expression ')'
En esa posición, los tokens legales son SEMICOLONo IN, pero no IDENT. Una palabra clave insería completamente inequívoca.
En este ejemplo en particular, los analizadores de arriba hacia abajo tampoco tendrían un problema, ya que podemos reescribir la gramática anterior para
for_loop = 'for' '(' declaration · for_loop_rest ')'
for_loop_rest = · ';' expression ';' expression
for_loop_rest = · 'in' expression
y todos los tokens necesarios para la decisión se pueden ver sin retroceder.
Considerar usabilidad
Java siempre ha tendido a la simplicidad semántica y sintáctica. Por ejemplo, el lenguaje no admite la sobrecarga del operador porque haría el código mucho más complicado. Entonces, al decidir entre iny :para una sintaxis de bucle for-each, tenemos que considerar cuál es menos confuso y más evidente para los usuarios. El caso extremo probablemente sería
for (in in in in())
for (in in : in())
(Nota: Java tiene espacios de nombres separados para nombres de tipos, variables y métodos. Creo que esto fue un error, principalmente. Esto no significa que el diseño del lenguaje posterior tenga que agregar más errores).
¿Qué alternativa proporciona separaciones visuales más claras entre la variable de iteración y la colección iterada? ¿Qué alternativa se puede reconocer más rápidamente cuando echas un vistazo al código? Descubrí que los símbolos de separación son mejores que una cadena de palabras cuando se trata de estos criterios. Otros idiomas tienen valores diferentes. Por ejemplo, Python deletrea muchos operadores en inglés para que se puedan leer de forma natural y sean fáciles de entender, pero esas mismas propiedades pueden dificultar la comprensión de una pieza de Python de un vistazo.