Comment un compilateur récupère-t-il exactement d'une erreur de type?


10

J'ai lu plusieurs articles, articles et section 4.1.4, chapitre 4 de Compilers: Principles, Techniques, and Tools (2nd Edition) (aka "The Dragon Book") qui traitent tous du sujet de la récupération d'erreur du compilateur syntaxique. Cependant, après avoir expérimenté avec plusieurs compilateurs modernes, j'ai vu qu'ils récupèrent également des erreurs sémantiques , ainsi que des erreurs syntaxiques.

Je comprends assez bien les algorithmes et techniques derrière les compilateurs récupérant des erreurs liées syntaxiquement, mais je ne comprends pas exactement comment un compilateur peut récupérer d'une erreur sémantique.

J'utilise actuellement une légère variation du modèle de visiteur pour générer du code à partir de mon arbre de syntaxe abstraite. Considérez mon compilateur qui compile les expressions suivantes:

1 / (2 * (3 + "4"))

Le compilateur générerait l'arborescence de syntaxe abstraite suivante:

      op(/)
        |
     -------
    /       \ 
 int(1)    op(*)
             |
          -------
         /       \
       int(2)   op(+)
                  |
               -------
              /       \
           int(3)   str(4)

La phase de génération de code utiliserait alors le modèle de visiteur pour parcourir récursivement l'arborescence de syntaxe abstraite et effectuer une vérification de type. L'arbre de syntaxe abstraite serait parcouru jusqu'à ce que le compilateur atteigne la partie la plus interne de l'expression; (3 + "4"). Le compilateur vérifie ensuite chaque côté des expressions et constate qu'elles ne sont pas sémantiquement équivalentes. Le compilateur déclenche une erreur de type. Voici où réside le problème. Que doit faire maintenant le compilateur ?

Pour que le compilateur se remette de cette erreur et continue de vérifier le type des parties externes des expressions, il devrait renvoyer un type ( intou str) d'évaluation de la partie la plus interne de l'expression à la partie la plus interne suivante de l'expression. Mais il n'a tout simplement pas de type à renvoyer . Puisqu'une erreur de type s'est produite, aucun type n'a été déduit.

Une solution possible que j'ai postulée, est que si une erreur de type se produit, une erreur doit être déclenchée et une valeur spéciale qui signifie qu'une erreur de type s'est produite doit être renvoyée aux appels précédents de traversée de l'arbre de syntaxe abstraite. Si les appels de parcours précédents rencontrent cette valeur, ils savent qu'une erreur de type s'est produite plus profondément dans l'arbre de syntaxe abstraite et doivent éviter d'essayer de déduire un type. Bien que cette méthode semble fonctionner, elle semble être très inefficace. Si la partie la plus à l'intérieur d'une expression est au fond de l'arbre de syntaxe abstraite, alors le compilateur devra effectuer de nombreux appels récursifs uniquement pour se rendre compte qu'aucun travail réel ne peut être effectué, et simplement revenir de chacun.

Est-ce que la méthode que j'ai décrite ci-dessus est utilisée (j'en doute). Si oui, n'est-ce pas efficace? Sinon, quelles sont exactement les méthodes utilisées lorsque les compilateurs récupèrent des erreurs sémantiques?


3
Je suis presque sûr que c'est ce qui est utilisé, et pourquoi ne pensez-vous pas que c'est assez efficace? Pour effectuer la vérification de type, le compilateur doit de toute façon parcourir l'arborescence entière . Un échec sémantique est plus efficace car il permet au compilateur d'éliminer une branche une fois l'erreur trouvée.
Telastyn

Réponses:


8

L'idée que vous proposez est essentiellement correcte.

La clé est que le type d'un nœud AST est calculé une seule fois puis stocké. Chaque fois que le type est à nouveau nécessaire, il récupère simplement le type stocké. Si la résolution se termine par une erreur, un type d'erreur est stocké à la place.


3

Une approche intéressante consiste à avoir un type spécial pour les erreurs. Lorsqu'une telle erreur est rencontrée pour la première fois, un diagnostic est enregistré et le type d'erreur est renvoyé en tant que type de l'expression. Ce type d'erreur a des propriétés intéressantes:

  • Toute opération qui y est effectuée réussit (afin d'éviter une cascade de messages d'erreur tous causés par le même défaut d'origine)
  • Le résultat de toute opération effectuée sur un objet avec un type d'erreur a également un type d'erreur
  • Si un type d'erreur va jusqu'à la génération de code, le générateur de code détecte l'utilisation et génère du code qui échoue (par exemple, déclenche une exception, abandonne ou tout ce qui est approprié pour votre langue)

Avec cette combinaison, vous pouvez réellement compiler avec succès du code qui contient des erreurs de type et tant que ce code n'est pas réellement utilisé, aucune erreur d'exécution ne se produit. Cela peut être utile, par exemple, pour vous permettre d'exécuter des tests unitaires pour les parties du code qui ne sont pas affectées.


Merci pour la réponse Jules. Assez drôle, c'est la méthode exacte que j'ai fini par utiliser. Les grands esprits pensent de la même façon, hein? ;-)
Christian Dean

2

S'il y a une erreur sémantique, un message d'erreur de compilation l'indiquant est émis à l'utilisateur.

Une fois cela fait, il est possible d'interrompre la compilation car le programme d'entrée est en erreur - ce n'est pas un programme légal dans la langue, il peut donc simplement être rejeté.

C'est assez dur, cependant, il existe donc des alternatives plus douces. Abandonnez toute génération de code et génération de fichier de sortie, mais continuez quelque chose pour rechercher plus d'erreurs.

Par exemple, il peut simplement abandonner toute analyse de type supplémentaire pour l'arborescence d'expression actuelle et continuer à traiter les expressions des instructions suivantes.


2

Supposons que votre langage autorise l'ajout d'entiers et la concaténation de chaînes avec l' +opérateur.

Étant donné que int + stringn'est pas autorisé, l'évaluation de la +entraînera une erreur signalée. Le compilateur pourrait simplement retourner en errortant que type. Ou il peut être plus intelligent, car int + int -> intet string + string -> stringsont autorisés, il peut retourner "erreur, peut être int ou chaîne".

Vient ensuite l' *opérateur, et nous supposerons que seul int + intest autorisé. Le compilateur peut alors décider que le +fait était censé retourner int, et le type renvoyé pour le *serait alors int, sans aucun message d'erreur.


Je pense que je vous suis, @gnasher, mais que voulez-vous dire exactement par l' opérateur "" ? C'était une faute de frappe?
Christian Dean

@ChristianDean il y a un astérisque dans les guillemets qui est interprété comme un balisage Markdown au lieu d'être rendu.
JakeRobb

J'ai soumis une modification à la réponse qui résoudra le problème dès que ma modification sera évaluée par des pairs.
JakeRobb
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.