web-dev-qa-db-fra.com

C ++ est-il sans contexte ou sensible au contexte?

J'entends souvent dire que C++ est un langage sensible au contexte. Prenons l'exemple suivant:

a b(c);

Est-ce une définition de variable ou une déclaration de fonction? Cela dépend de la signification du symbole c. Si c est un variable, alors a b(c); définit une variable nommée b de type a. Il est directement initialisé avec c. Mais si c est un type, alors a b(c); déclare une fonction nommée b qui prend un c et retourne un a.

Si vous consultez la définition des langages sans contexte, vous constaterez en gros que toutes les règles de grammaire doivent avoir un côté gauche composé d'un seul symbole non terminal. Les grammaires contextuelles, quant à elles, autorisent des chaînes arbitraires de symboles terminaux et non terminaux du côté gauche.

En parcourant l'annexe A du "Langage de programmation C++", je ne trouvais pas de règle de grammaire unique comportant autre chose qu'un symbole non terminal sur son côté gauche. Cela impliquerait que C++ est sans contexte. (Bien entendu, chaque langue sans contexte est également sensible au contexte en ce sens que les langages sans contexte constituent un sous-ensemble des langages sensibles au contexte, mais ce n'est pas le problème.)

Donc, C++ est-il sans contexte ou sensible au contexte?

393
fredoverflow

Vous trouverez ci-dessous ma démonstration (actuelle) préférée de la raison pour laquelle l'analyse de C++ est (probablement) Turing-complete , car elle affiche un programme dont la syntaxe est correcte si et seulement si un entier donné est premier.

J'affirme donc que C++ n'est ni dépourvu de contexte ni sensible au contexte .

Si vous autorisez des séquences de symboles arbitraires des deux côtés d'une production, vous créez une grammaire de type 0 ("sans restriction") dans hiérarchie de Chomsky , qui est plus puissante qu'une grammaire sensible au contexte; les grammaires sans restriction sont Turing-complete. Une grammaire contextuelle (Type-1) autorise plusieurs symboles de contexte sur le côté gauche d'une production, mais le même contexte doit apparaître sur le côté droit de la production (d'où le nom "sensible au contexte"). [1] Les grammaires contextuelles sont équivalentes à machines de Turing à bornes linéaires .

Dans l'exemple de programme, le calcul de prime pourrait être effectué par une machine de Turing à bornes linéaires, de sorte qu'il ne prouve pas tout à fait l'équivalence de Turing, mais l'important est que l'analyseur doit effectuer le calcul pour pouvoir effectuer une analyse syntaxique. Cela aurait pu être n'importe quel calcul exprimable sous forme d'instanciation de modèle et il y a tout lieu de croire que l'instanciation de modèle C++ est Turing-complete. Voir, par exemple, article de Todd L. Veldhuizen, 20 .

Quoi qu'il en soit, C++ peut être analysé par un ordinateur, il pourrait donc être analysé par une machine de Turing. Par conséquent, une grammaire sans restriction pourrait le reconnaître. En fait, écrire une telle grammaire serait peu pratique, c'est pourquoi la norme n'essaie pas de le faire. (Voir ci-dessous.)

Le problème de "l'ambiguïté" de certaines expressions est principalement un fil rouge. Pour commencer, l'ambiguïté est une caractéristique d'une grammaire particulière, pas une langue. Même s'il est prouvé qu'une langue ne possède pas de grammaires sans ambiguïté, si elle peut être reconnue par une grammaire sans contexte, elle est sans contexte. De même, si elle ne peut pas être reconnue par une grammaire sans contexte, mais par une grammaire sensible au contexte, elle est sensible au contexte. L'ambiguïté n'est pas pertinente.

Mais dans tous les cas, comme la ligne 21 (c'est-à-dire auto b = foo<IsPrime<234799>>::typen<1>();) dans le programme ci-dessous, les expressions ne sont pas du tout ambiguës; ils sont simplement analysés différemment selon le contexte. Dans la plus simple expression du problème, la catégorie syntaxique de certains identificateurs dépend de la façon dont ils ont été déclarés (types et fonctions, par exemple), ce qui signifie que le langage formel devrait reconnaître le fait que deux chaînes de longueur arbitraire dans le même programme est identique (déclaration et utilisation). Ceci peut être modélisé par la grammaire "copie", qui est la grammaire qui reconnaît deux copies exactes consécutives du même mot. Il est facile de prouver avec lemme de pompage que ce langage n’est pas dépourvu de contexte. Une grammaire contextuelle pour cette langue est possible et une grammaire de type 0 est fournie dans la réponse à cette question: https://math.stackexchange.com/questions/163830/context-sensitive-grammar- pour la langue de la copie .

Si l'on essayait d'écrire une grammaire sensible au contexte (ou sans restriction) pour analyser le C++, cela remplirait probablement l'univers de gribouillis. Écrire une machine de Turing pour analyser le C++ serait une entreprise tout aussi impossible. Même écrire un programme C++ est difficile et, autant que je sache, aucun n’a été prouvé. C'est pourquoi la norme ne tente pas de fournir une grammaire formelle complète, et pourquoi elle choisit d'écrire certaines des règles d'analyse syntaxique en anglais technique.

Ce qui ressemble à une grammaire formelle dans la norme C++ n’est pas la définition formelle complète de la syntaxe du langage C++. Ce n'est même pas la définition formelle complète de la langue après le prétraitement, qui pourrait être plus facile à formaliser. (Cela ne serait pas le langage, cependant: le langage C++ défini par le standard inclut le préprocesseur, et le fonctionnement du préprocesseur est décrit de manière algorithmique car il serait extrêmement difficile à décrire dans un formalisme grammatical. C'est dans cette section de la norme où la décomposition lexicale est décrite, y compris les règles où elle doit être appliquée plus d’une fois.)

Les différentes grammaires (deux grammaires qui se chevauchent pour l'analyse lexicale, l'une qui a lieu avant le prétraitement et l'autre éventuellement, plus tard, plus la grammaire "syntaxique") sont rassemblées dans l'Annexe A, avec cette note importante (soulignement ajouté):

Ce résumé de la syntaxe C++ se veut une aide à la compréhension. Ce n'est pas une déclaration exacte de la langue. En particulier, la grammaire décrite ici accepte un super ensemble de constructions C++ valides. Les règles de désambiguïsation (6.8, 7.1, 10.2) doivent être appliquées pour distinguer les expressions des déclarations. En outre, le contrôle d'accès, l'ambiguïté et les règles de type doivent être utilisés pour éliminer les constructions syntaxiquement valables mais sans signification.

Enfin, voici le programme promis. La ligne 21 est syntaxiquement correcte si et seulement si le N dans IsPrime<N> est premier. Sinon, typen est un entier et non un modèle. Ainsi, typen<1>() est analysé comme (typen<1)>(), ce qui est syntaxiquement incorrect, car () n'est pas une expression syntaxiquement valide.

template<bool V> struct answer { answer(int) {} bool operator()(){return V;}};

template<bool no, bool yes, int f, int p> struct IsPrimeHelper
  : IsPrimeHelper<p % f == 0, f * f >= p, f + 2, p> {};
template<bool yes, int f, int p> struct IsPrimeHelper<true, yes, f, p> { using type = answer<false>; };
template<int f, int p> struct IsPrimeHelper<false, true, f, p> { using type = answer<true>; };

template<int I> using IsPrime = typename IsPrimeHelper<!(I&1), false, 3, I>::type;
template<int I>
struct X { static const int i = I; int a[i]; }; 

template<typename A> struct foo;
template<>struct foo<answer<true>>{
  template<int I> using typen = X<I>;
};
template<> struct foo<answer<false>>{
  static const int typen = 0;
};

int main() {
  auto b = foo<IsPrime<234799>>::typen<1>(); // Syntax error if not prime
  return 0;
}

[1] Pour le dire plus techniquement, chaque production dans une grammaire contextuelle doit être de la forme:

αAβ → αγβ

A est un non-terminal et α, β sont éventuellement des séquences vides de symboles de grammaire et γ est une séquence non-vide. (Les symboles de grammaire peuvent être des terminaux ou des non-terminaux).

Ceci peut être lu comme A → γ uniquement dans le contexte [α, β]. Dans une grammaire sans contexte (type 2), α et β doivent être vides.

Il s'avère que vous pouvez également restreindre les grammaires avec la restriction "monotone", où chaque production doit être de la forme:

α → β|α| ≥ |β| > 0 (|α| signifie "la longueur de α")

Il est possible de prouver que l'ensemble des langages reconnus par les grammaires monotones est exactement le même que celui des grammaires sensibles au contexte, et il est souvent plus facile de baser des preuves sur des grammaires monotones. Par conséquent, il est assez courant de voir "context sensible" utilisé comme si cela signifiait "monotone".

331
rici

Premièrement, vous avez observé à juste titre qu’il n’existait pas de règles sensibles au contexte dans la grammaire à la fin du standard C++, de sorte que la grammaire est est dépourvue de contexte.

Cependant, cette grammaire ne décrit pas précisément le langage C++, car elle produit des programmes non-C++ tels que

int m() { m++; }

ou

typedef static int int;

Le langage C++ défini comme "l'ensemble de programmes C++ bien formés" n'est pas dépourvu de contexte (il est possible de montrer que le simple fait d'exiger des variables à déclarer le rend ainsi). Étant donné que vous pouvez théoriquement écrire des programmes complets avec Turing dans des modèles et rendre un programme mal formé en fonction de ses résultats, il n’est même pas sensible au contexte.

Maintenant, les personnes (ignorantes) (généralement pas des théoriciens du langage, mais des concepteurs d'analyseurs) utilisent généralement le terme "sans contexte" dans certaines des significations suivantes

  • ambigu
  • ne peut pas être analysé avec Bison
  • pas LL (k), LR (k), LALR (k) ou la classe de langue définie par l'analyseur qu'ils ont choisie

La grammaire à la fin de la norme ne satisfait pas ces catégories (c’est-à-dire qu’elle est ambiguë, pas LL (k) ...), donc la grammaire C++ n’est pas "sans contexte". Et dans un sens, ils ont raison, il est bien difficile de produire un analyseur syntaxique C++ fonctionnel.

Notez que les propriétés utilisées ici ne sont que faiblement liées aux langages sans contexte - l'ambiguïté n'a rien à voir avec la sensibilité au contexte (en fait, les règles sensibles au contexte aident généralement à la désambiguïsation des productions), les deux autres ne sont que des sous-ensembles du contexte. - langues libres. Et analyser les langues sans contexte n'est pas un processus linéaire (bien que l'analyse des déterministes le soit).

113
jpalecek

Oui. L'expression suivante a un différent ordre des opérations en fonction de type contexte résol:

Edit: Lorsque l'ordre réel des opérations varie, il est extrêmement difficile d'utiliser un compilateur "normal" qui analyse un AST non décoré avant de le décorer (propagation des informations de type). Les autres éléments sensibles au contexte mentionnés sont "plutôt faciles" par rapport à cela (l'évaluation des modèles n'est pas du tout facile).

#if FIRST_MEANING
   template<bool B>
   class foo
   { };
#else
   static const int foo = 0;
   static const int bar = 15;
#endif

Suivi par:

static int foobar( foo < 2 ? 1 < 1 : 0 > & bar );
60
Sam Harwell

Pour répondre à votre question, vous devez distinguer deux questions différentes.

  1. La syntaxe de presque tous les langages de programmation est sans contexte. Typiquement, il est donné sous forme de Backus-Naur étendu ou de gramar sans contexte.

  2. Cependant, même si un programme est conforme à la gramar sans contexte définie par le langage de programmation, il ne s’agit pas nécessairement d’un programme valide. Un programme doit satisfaire plusieurs types de propriétés non dépourvues de contexte pour être un programme valide. Par exemple, la propriété la plus simple de ce type est la portée des variables.

Pour conclure, le fait que C++ soit ou non dépendant du contexte dépend de la question que vous posez.

24
Dan

Oui, C++ est sensible au contexte, très sensible au contexte. Vous ne pouvez pas construire l'arbre de syntaxe en analysant simplement le fichier à l'aide d'un analyseur syntaxique sans contexte car, dans certains cas, vous devez connaître le symbole de la connaissance précédente pour pouvoir décider (par exemple, créer une table de symboles lors de l'analyse).

Premier exemple:

A*B;

Est-ce une expression de multiplication?

OR

S'agit-il d'une déclaration de la variable B sous la forme d'un pointeur de type A?

Si A est une variable, alors c'est une expression, si A est un type, c'est une déclaration de pointeur.

Deuxième exemple:

A B(bar);

S'agit-il d'un prototype de fonction prenant un argument de type bar?

OR

Cette variable de déclaration B est-elle de type A et appelle-t-elle le constructeur de A avec bar constante comme initialiseur?

Vous devez savoir à nouveau si bar est une variable ou un type de la table des symboles.

Troisième exemple:

class Foo
{
public:
    void fn(){x*y;}
    int x, y;
};

C'est le cas lorsque la construction d'une table de symboles lors d'une analyse n'aide pas, car la déclaration de x et y vient après la définition de la fonction. Vous devez donc commencer par parcourir la définition de classe, puis consulter les définitions de méthodes lors d'une seconde passe, pour dire que x * y est une expression et non une déclaration de pointeur, etc.

12
Calmarius

Vous voudrez peut-être jeter un oeil à Conception et évolution du C++ , par Bjarne Stroustrup. Il y décrit ses problèmes en essayant d'utiliser yacc (ou similaire) pour analyser une version antérieure de C++, et en souhaitant qu'il utilise plutôt la descente récursive.

12
anon

C++ est analysé avec l'analyseur GLR. Cela signifie que lors de l’analyse du code source, l’analyseur peut rencontrer une ambiguïté, mais il doit continuer et choisir la règle de grammaire à utiliser plus tard .

regarde aussi

Pourquoi C++ ne peut-il pas être analysé avec un analyseur syntaxique LR (1)?


Rappelez-vous que la grammaire sans contexte ne peut pas décrire TOUS les règles d'une syntaxe de langage de programmation. Par exemple, Attribute Grammar est utilisé pour vérifier la validité d'un type d'expression.

int x;
x = 9 + 1.0;

Vous ne pouvez pas décrire la règle suivante avec une grammaire sans contexte: Le côté droit de l'assignation doit être du même type que celui de la main gauche. côté

10
AraK

J'ai l'impression qu'il y a une certaine confusion entre la définition formelle de "sensible au contexte" et l'utilisation informelle de "sensible au contexte". Le premier a un sens bien défini. Ce dernier est utilisé pour dire "vous avez besoin de contexte pour analyser l'entrée".

Ceci est également demandé ici: Sensibilité au contexte vs ambiguïté .

Voici une grammaire sans contexte:

<a> ::= <b> | <c>
<b> ::= "x"
<c> ::= "x"

C'est ambigu, donc pour analyser l'entrée "x", vous avez besoin d'un contexte (ou vivez avec l'ambiguïté, ou émettez "Avertissement: E8271 - L'entrée est ambiguë dans la ligne 115"). Mais ce n'est certainement pas une grammaire contextuelle.

9
Omri Barel

Aucun langage de type ALGOL n'est dépourvu de contexte, car ils ont des règles qui contraignent les expressions et les instructions dans lesquelles les identificateurs peuvent apparaître en fonction de leur type, et parce qu'il n'y a pas de limite au nombre d'instructions pouvant exister entre déclaration et utilisation.

La solution habituelle consiste à écrire un analyseur syntaxique sans contexte qui accepte réellement un sur-ensemble de programmes valides et à placer les parties sensibles au contexte dans ad hoc "sémantique". code attaché aux règles.

C++ va bien au-delà, grâce à son système de templates Turing-complete. Voir Question de débordement de pile 794015 .

6
James Jones

Les productions dans la norme C++ sont écrites sans contexte, mais comme nous le savons tous, le langage n'est pas défini avec précision. Une partie de ce que la plupart des gens considèrent comme une ambiguïté dans le langage actuel pourrait (je crois) être résolue sans ambiguïté avec une grammaire sensible au contexte.

Pour l’exemple le plus évident, considérons l’analyse la plus vexante: int f(X);. Si X est une valeur, cela définit alors f comme une variable qui sera initialisée avec X. Si X est un type, il définit f comme une fonction prenant un seul paramètre de type X.

En regardant cela d'un point de vue grammatical, nous pourrions le voir comme ceci:

A variable_decl ::= <type> <identifier> '(' initializer ')' ';'

B function_decl ::= <type> <identifier> '(' param_decl ')' ';'

A ::= [declaration of X as value]
B ::= [declaration of X as type]

Bien sûr, pour être tout à fait exact, nous aurions besoin d'ajouter quelques "éléments" supplémentaires afin de tenir compte de la possibilité d'intervenir dans des déclarations d'autres types (c'est-à-dire, A et B devraient en réalité être des "déclarations comprenant la déclaration de X en tant que ..." , ou quelque chose dans cet ordre).

C’est quand même assez différent d’un CSG typique (ou du moins de ce dont je me souviens). Cela dépend de la construction d'une table de symboles - la partie qui reconnaît spécifiquement X en tant que type ou valeur, non pas simplement un type d'instruction précédant celui-ci, mais le type d'instruction correct pour le symbole/identificateur correct.

En tant que tel, je devrais faire quelques recherches pour en être sûr, mais ma supposition immédiate est que cela ne constitue pas vraiment une CSG, du moins tel que le terme est normalement utilisé.

5
Jerry Coffin

Le cas le plus simple de grammaire non dépourvue de contexte implique l’analyse d’expressions impliquant des modèles.

a<b<c>()

Cela peut analyser soit

template
   |
   a < expr > ()
        |
        <
      /   \
     b     c

Ou

 expr
   |
   <
 /   \
a   template
     |
     b < expr > ()
          |
          c

Les deux AST ne peuvent être désambiguïsés qu'en examinant la déclaration 'a' - l'ancien AST si 'a' est un modèle, ou le dernier sinon.

5
Aaron

Il est sensible au contexte, car a b(c); a deux déclarations syntaxiques et variables valides. Quand vous dites "Si c est un type", c'est le contexte, juste ici, et vous décrivez exactement à quel point C++ y est sensible. Si vous n'aviez pas ce contexte de "Qu'est-ce que c?" vous ne pouvez pas analyser cela sans ambiguïté.

Ici, le contexte est exprimé dans le choix des jetons. L'analyseur lit un identifiant sous la forme d'un jeton nom_type s'il nomme un type. Il s'agit de la résolution la plus simple et évite en grande partie la complexité d'être sensible au contexte (dans ce cas).

Edit: Il y a bien sûr plus de problèmes de sensibilité au contexte, je me suis concentré sur celui que vous avez montré. Les modèles sont particulièrement méchants pour cela.

5
Puppy

Vrai :)

J. Stanley Warford. Systèmes informatiques . Pages 341-346.

5
anno

Les modèles C++ se sont révélés puissants. Bien qu'il ne s'agisse pas d'une référence formelle, voici un endroit à regarder à cet égard:

http://cpptruths.blogspot.com/2005/11/c-templates-are-turing-complete.html

Je vais tenter une hypothèse (aussi vieille qu'une preuve folklorique et concise du MCAC montrant qu'ALGOL dans les années 60 ne pouvait pas être représenté par un CFG) et dire que le C++ ne pouvait donc pas être correctement analysé uniquement par un CFG. Les CFG, en conjonction avec divers mécanismes TP, dans une passe d'arbre ou pendant des événements de réduction - c'est une autre histoire. D'une manière générale, en raison du problème Halting, il existe certains programmes C++ qui ne peuvent pas être considérés comme corrects/incorrects mais qui sont néanmoins corrects/incorrects.

{PS- En tant qu'auteur de Meta-S (mentionné par plusieurs personnes ci-dessus) - Je peux assurément affirmer que Thothic n'est ni ancien, ni logiciel disponible gratuitement. J'ai peut-être formulé cette version de ma réponse de manière à ne pas être supprimé ou réduit à -3.}

4
4
sdcvvc

C++ n'est pas libre de contexte. Je l'ai appris il y a quelque temps dans une conférence sur les compilateurs. Une recherche rapide a donné ce lien, où la section "Syntaxe ou sémantique" explique pourquoi C et C++ ne sont pas libres de contexte:

Wikipedia Talk: Grammaire sans contexte

Cordialement,
Ovanes

3
ovanes

"Meta-S" est un moteur d'analyse contextuelle de Quinn Tyler Jackson. Je ne l'ai pas utilisé, mais il raconte une histoire impressionnante. Découvrez ses commentaires dans comp.compilers et visitez le site rnaparse.com/MetaS%20defined.htm - Ira Baxter 25 juillet à 10:42

Le lien correct est analyse des énigines

Meta-S était la propriété d'une ancienne société appelée Thothic. Je peux envoyer une copie gratuite du Meta-S à toute personne intéressée et je l’ai utilisée dans la recherche sur l’analyse syntaxique. Veuillez noter que la "grammaire pseudoknot" incluse dans les exemples de dossiers a été écrite par un programmeur non bioinformatique, amateur, et ne fonctionne pas. Mes grammaires ont une approche différente et fonctionnent assez bien.

2
user175479

Évidemment, si vous prenez la question à la lettre, presque toutes les langues avec des identifiants sont sensibles au contexte.

Il faut savoir si un identifiant est un nom de type (un nom de classe, un nom introduit par typedef, un paramètre template nomtype), un nom de modèle ou un autre nom pour pouvoir correctement utiliser l’identifiant. Par exemple:

x = (name)(expression);

est un cast si name est un nom de type et un appel de fonction si name est un nom de fonction. Un autre cas est ce que l'on appelle "l'analyse la plus frustrante" où il n'est pas possible de différencier la définition de variable et la déclaration de fonction (il existe une règle spécifiant qu'il s'agit d'une déclaration de fonction).

Cette difficulté a introduit le besoin de typename et template avec des noms dépendants. Autant que je sache, le reste de C++ n’est pas sensible au contexte (c’est-à-dire qu’il est possible d’écrire une grammaire sans contexte).

2
AProgrammer

Un gros problème ici est que les termes "sans contexte" et "sensible au contexte" sont un peu peu intuitifs en informatique. Pour C++, la sensibilité au contexte ressemble beaucoup à l'ambiguïté, mais ce n'est pas nécessairement vrai dans le cas général.

En C/++, une instruction if n'est autorisée qu'à l'intérieur d'un corps de fonction. Cela semblerait rendre le contexte sensible, non? Et bien non. Les grammaires sans contexte n'ont pas réellement besoin de la propriété où vous pouvez extraire une ligne de code et déterminer si elle est valide. Ce n'est pas réellement ce que signifie sans contexte. C'est vraiment juste une étiquette qui implique vaguement quelque chose de lié à ce que cela ressemble.

Maintenant, si une instruction dans un corps de fonction est analysée différemment en fonction de quelque chose défini en dehors des ancêtres grammaticaux immédiats (par exemple, si un identifiant décrit un type ou une variable), comme dans le cas a * b;, il s'agit en fait d'un contexte -sensible. Il n'y a pas d'ambiguïté réelle ici; il sera analysé comme une déclaration de pointeur si a est un type et une multiplication dans le cas contraire.

Être sensible au contexte ne signifie pas nécessairement "difficile à analyser". C n’est en réalité pas si difficile car l’infâme a * b; "ambiguïté" peut être résolu avec une table de symboles contenant typedefs rencontrés précédemment. Il ne nécessite aucune instanciation arbitraire de modèle (il a été prouvé que Turing Complete) permet de résoudre ce problème comme le fait parfois C++. Il n'est pas réellement possible d'écrire un programme C qui ne compilera pas dans un laps de temps déterminé, même s'il a la même sensibilité au contexte que le C++.

Python (et d'autres langages sensibles aux espaces) dépend également du contexte, car il nécessite un état dans le lexer pour générer des jetons d'indentation et de dédentation, mais cela ne rend pas plus difficile l'analyse d'une grammaire LL-1 typique. Il utilise en fait un générateur d’analyseurs, ce qui explique en partie pourquoi Python a de tels messages d’erreur de syntaxe peu informative. Il est également important de noter ici qu'il n'y a pas "d'ambiguïté" comme le problème a * b; de Python, donnant un bon exemple concret d'un langage sensible au contexte sans grammaire "ambiguë" (comme mentionné dans le premier paragraphe).

0
Beefster