Une langue qui est LR(1) mais pas LL(*)
La question Comparaison théorique des grammaires LL et LR dispose d'une réponse avec la langue suivante qui est LR(1) mais pas LL(*):
{ a^i b^j | ij }
C'est-à-dire, un certain nombre de a
suivi par un nombre égal ou inférieur de b
.
La même langue est citée par cette réponse à la question similaire Exemple d'une grammaire LR qui ne peut pas être représentée par LL?. Cependant, la question présente est différente car l'autre mentionne "LL", signifiant LL(k), tandis que nous parlons ici de LL(*) (et Antlr4).
Démonstration intuitive (pas une preuve)
Expliquons intuitivement que ceci est LR(1) mais pas LL(*).
Tout d'abord, la grammaire LR(1) (copiée de la deuxième réponse liée):
S ::= a S | P
P ::= a P b |
De manière intuitive, ceci est LR(1) car un parseur LR(1) peut empiler n'importe quel nombre de symboles a
sur sa pile, puis, lorsqu'il arrive au premier b
, commencer à dépiler les symboles a
correspondants en paires a,b
, en utilisant la première production pour P
. S'il se retrouve sans symboles b
, il dépile les symboles a
restants en utilisant la première production pour S
. S'il se retrouve sans symboles a
alors qu'il reste des symboles b
, il signale une erreur. (Rappelez-vous, dans ce contexte, nous sommes principalement concernés par la reconnaissance, donc la sortie est soit "oui" soit "erreur".)
En revanche, cette grammaire n'est pas LL(*). De manière intuitive, un parseur LL(*) doit décider, lorsqu'il voit le premier a
, s'il doit utiliser la première ou la deuxième production de S
. Il aimerait anticiper pour voir s'il y a autant de symboles b
que de symboles a
restants, car dans le cas contraire, il saurait qu'il doit utiliser la première production pour "consommer" les symboles a
en trop. Mais la capacité de prévision de LL(*) est limitée à reconnaître un langage régulier, et un langage régulier ne peut pas reconnaître { a^i b^i }
puisqu'il ne peut pas "compter".
Bien sûr, le fait qu'une grammaire ne soit pas LL(*) n'implique pas que le langage ne soit pas LL(*), car il pourrait exister une grammaire plus astucieuse. Pour prouver qu'il n'est pas LL(*), je commencerais probablement par la définition formelle, supposerais que j'avais une grammaire avec ces conditions, puis utiliserais un argument de lemme du pompage pour montrer qu'elle ne peut pas reconnaître correctement le langage d'intérêt. Mais je laisserai aux ressources liées le soin de justifier rigoureusement que le langage n'est pas LL(*).
Interprétation à un niveau supérieur
La façon dont je le vois, LL prend des décisions en descendant dans l'arbre d'analyse, alors que LR les prend en remontant. Pour créer un langage qui n'est pas LL(k), nous disposons les choses de manière à ce qu'un parseur hypothétique doive s'engager dans une interprétation d'un symbole lorsque l'information nécessaire pour le faire est au-delà de l'horizon de k
symboles. Pour le rendre pas LL(*), nous devons placer l'information cruciale au-delà d'un horizon qui ne peut être franchi qu'en reconnaissant d'abord un langage non régulier.
En revanche, LR peut empiler des symboles, reportant leur interprétation jusqu'à ce qu'il ait vu la fin de la production associée et déjà construit des interprétations de tout ce qui se trouve entre les deux.
Pour essayer de rendre ceci un peu plus concret, imaginez un langage de programmation qui a deux types de choses entre accolades, disons, des blocs de code et des littéraux d'objet (comme en Javascript). Imaginez qu'ils peuvent tous les deux apparaître dans le même contexte (contrairement à Javascript):
var x = { console.log("Je suis un bloc de code"); /*le résultat est*/ 6; };
var x = { a:1, b:2 };
Dans ce contexte, un parseur rencontre {
. LL doit décider immédiatement s'il s'agit du début d'un bloc de code ou d'un littéral d'objet. En Javascript, les clés des littéraux d'objets doivent être des identifiants ou des littéraux de chaîne, et l'union des deux est un langage régulier, donc un parseur LL(*) peut passer rapidement à côté de l'expression régulière pour "identifiant ou littéral de chaîne" pour vérifier s'il y a un :
, ce qui signalerait un littéral d'objet (sinon bloc de code).
{ // hmm, code ou objet?
{ a // clé possible de littéral objet
{ a : // a-ha! définitivement littéral objet
S'il s'agissait plutôt d'une clé pouvant être une expression de type chaîne arbitraire, alors LL(*) est en difficulté car il doit équilibrer les parenthèses pour passer la clé présumée afin de vérifier le :
:
{ // début de littéral objet?
{ ( // oh là là ...
{ (a // je suis
{ (a ? // en train de me
{ (a ? b // perdre
{ (a ? b : // est-ce le ':' après une clé? aider!
En revanche, LR reporte volontiers l'interprétation de {
, l'empilant, et procède, en fait, avec deux interprétations potentielles jusqu'à ce qu'un jeton les désambiguïse.
J'espère que cela fournit une certaine intuition sur les types de choses que LR contient mais pas LL(*)
Il existe des exemples de l'inverse (LL(*) mais pas LR), bien que je ne sache pas à quoi ils ressemblent ("pas LR" est une classe difficile à imaginer); voir la première question liée pour plus de détails.
Prédicats sémantiques Antlr4
Maintenant, le titre de la question demande en réalité à propos d'Antlr4. Antlr4 dispose de prédicats sémantiques, permettant efficacement au programmeur d'insérer un calcul de prévision arbitraire. Donc, si vous êtes prêt à sortir du formalisme de la grammaire, en réalité il n'y a pas de limite (à part la décidabilité) à ce qu'un parseur Antlr4 peut reconnaître.