Intentaré ponerlo en términos simples.
Si piensa en términos del árbol de análisis (no el AST, sino la visita del analizador y la expansión de la entrada), la recursión izquierda da como resultado un árbol que crece hacia la izquierda y hacia abajo. La recursión correcta es exactamente lo contrario.
Como ejemplo, una gramática común en un compilador es una lista de elementos. Tomemos una lista de cadenas ("rojo", "verde", "azul") y analicemosla. Podría escribir la gramática de varias maneras. Los siguientes ejemplos son recursivos directamente hacia la izquierda o hacia la derecha, respectivamente:
arg_list: arg_list:
STRING STRING
| arg_list ',' STRING | STRING ',' arg_list
Los árboles para estos analizan:
(arg_list) (arg_list)
/ \ / \
(arg_list) BLUE RED (arg_list)
/ \ / \
(arg_list) GREEN GREEN (arg_list)
/ /
RED BLUE
Tenga en cuenta cómo crece en la dirección de la recursión.
Esto no es realmente un problema, está bien querer escribir una gramática recursiva izquierda ... si su herramienta de análisis puede manejarlo. Los analizadores de abajo hacia arriba lo manejan bien. Así pueden los analizadores LL más modernos. El problema con las gramáticas recursivas no es la recursividad, es la recursividad sin avanzar el analizador, o recurrir sin consumir una ficha. Si siempre consumimos al menos 1 token cuando repetimos, eventualmente llegamos al final del análisis. La recursión izquierda se define como recurrir sin consumir, que es un bucle infinito.
Esta limitación es puramente un detalle de implementación de implementar una gramática con un ingenuo analizador LL de arriba hacia abajo (analizador recursivo de descenso). Si desea seguir con las gramáticas recursivas izquierdas, puede lidiar con eso reescribiendo la producción para consumir al menos 1 token antes de recurrir, por lo que esto asegura que nunca nos atasquemos en un ciclo no productivo. Para cualquier regla gramatical que sea recursiva a la izquierda, podemos reescribirla agregando una regla intermedia que aplana la gramática a un solo nivel de búsqueda anticipada, consumiendo una ficha entre las producciones recursivas. (NOTA: No estoy diciendo que esta sea la única forma o la forma preferida de reescribir la gramática, solo señalando la regla generalizada. En este ejemplo simple, la mejor opción es usar la forma recursiva correcta). Como este enfoque es generalizado, un generador de analizadores puede implementarlo sin involucrar al programador (teóricamente). En la práctica, creo que ANTLR 4 ahora hace exactamente eso.
Para la gramática anterior, la implementación de LL que muestra la recursión izquierda se vería así. El analizador comenzaría con la predicción de una lista ...
bool match_list()
{
if(lookahead-predicts-something-besides-comma) {
match_STRING();
} else if(lookahead-is-comma) {
match_list(); // left-recursion, infinite loop/stack overflow
match(',');
match_STRING();
} else {
throw new ParseException();
}
}
En realidad, lo que realmente estamos tratando es "implementación ingenua", es decir. inicialmente predicamos una oración dada, luego recurrimos recursivamente a la función para esa predicción, y esa función ingenuamente llama a la misma predicción nuevamente.
Los analizadores ascendentes no tienen el problema de las reglas recursivas en ninguna dirección, ya que no vuelven a analizar el comienzo de una oración, funcionan al volver a unir la oración.
La recursividad en una gramática es solo un problema si producimos de arriba hacia abajo, es decir. nuestro analizador funciona al "expandir" nuestras predicciones a medida que consumimos tokens. Si en lugar de expandirnos, colapsamos (las producciones se "reducen"), como en un analizador ascendente LALR (Yacc / Bison), entonces la recurrencia de cualquier lado no es un problema.
::=
deExpression
aTerm
, y si hiciera lo mismo después del primero||
, ¿ya no sería recursivo a la izquierda? ¿Pero que si solo lo hicieras después::=
, pero no||
, aún sería recursivo?