Analyse sémantique : la table des symboles

Nous utiliserons principalement la table des symboles lors de l'analyse sémantique, mais nous la construirons pendant notre analyse syntaxique. Nous pouvons aussi profiter de la construction de la table des symboles poçur détecter des erreurs de syntaxe et décider d'interrompre le processus de compilation.

Dans ce document, nous allons nous poser les questions préliminaires nécessaires à la création d'une table des symboles pour le compilateur. Le document suivant reprend une implémentation possible en fonction des choix que nous prendrons suite à ces questions.

Problème de portée et de surcharge

Comme nous l'avons constaté lors de nos recherches d'une structure de données adéquoite pour notre table des symboles, nous retrouverons souvent un même identificateur pour des variables ou des méthodes1. Nous devrons donc trouver un moyen de gérer ces problèmes de portée et de surcharge.

Portée

Le problème de portée d'un symbole est comme un ensemble de poupées russes : une série de blocs qui peuvent contenir d'autres blocs. Si nous ne trouvons pas la déclaration de la variable dans le bloc dans lequel elle est utilisée, nous devons vérifier dans le bloc parent. C'est la règle de « l'englobant le plus proche », qui stipule que nous cherchons de bloc englobant en bloc englobant (ce qui correspond à une descente vers le fond de notre pile), et que la première déclaration trouvée est celle qui s'applique au bloc de départ de la recherche.

Si nous sommes au fond de la pile et si nous n'avons pas encore trouvé la déclaration du symbole, nous avons une erreur d'utilisation de variable non déclarée, ou une erreur d'utilisation de fonction non déclarée.

Nous pouvons considérer les différentes portées comme une pile. Selon le type d'implémentation choisi, nous pouvons avoir dans chaque élément de la pile une table des symboles, et dans ce cas nous ne pouvons plus avoir de collisions (car chaque table des symboles est limitée correspond à une portée particulière).
Nous ne retiendrons pas ce type d'implémentation car nous utiliserons le langage C et créer un tableau pour chaque portée coûte énormément en mémoire. De plus, peu de cellules par tableau seront réellement utilisées.

Utilisation d'une table principale

Si nous développons un compilateur qui sera exécuté sur des machines qui possèdent beaucoup de ressources mémoire, nous pouvons accélérer les lectures dans la table des symboles en créant une table qui contient les symboles dont la visibilité n'est pas masquée par des problèmes de portée. Nous devons alors maintenir une information supplémentaire dans cette table : la portée dans laquelle le symbole est déclaré. Ce gain de temps en lecture est balancé par une perte lors de l'insertion d'un symbole qui existe déjà : il faut alors non seulement copier le nouveau symbole dans la table qui correspond à sa portée, mais aussi marquer l'entrée dans la table principale afin que l'on sache qu'il est nécessaire de chercher dans la table correspondant à la bonne portée.

Table des matières Haut

Surcharge

Certains langages permettent d'utiliser un même nom pour différentes fonctions pour autant que leurs arguments diffèrent en nombre et/ou en types. Nous devons donc alors maintenir une collection comme valeur, car nous aurons des collisions au niveau des noms.

Signature de fonctions

Nous pouvons éviter ce genre de collision si nous utilisons un identifiant adapté à la « signature » de la méthode : par exemple une concaténation du nom de la fonction et des types de ses arguments dans l'ordre.

Table des matières Haut

Position des déclarations

Les règles relatives aux déclarations, imposées par le langage analysé, auront un impact sur la manière dont nous implémenterons notre table des symboles au sein du compilateur. En effet, certains langages permettent l'utilisation de variables non déclarées, d'autres requièrent une « déclaration anticipative » (en anglais, “backward declaration”), ou encore permettent une « déclaration a posteriori » (en anglais, “forward declaration”).

Nous pouvons travailler de la manière suivante pour détecter la position de la déclaration : déterminer dans quelle portée se trouvent la déclaration et l'utilisation du symbole, puis vérifier la position au sein du fichier source.

Table des matières Haut

Quand créer la table des symboles ?

Nous pouvons créer la table des symboles au début de notre compilation. Nous créerons les blocs de portées par la suite.

Table des matières Haut

Quand remplir la table des symboles ?

Nous pouvons attendre que l'AST [“Abstract Syntaxic Tree”4] soit entièrement constitué, et le parcourir ensuite pour constituer notre table des symboles, mais ce n'est pas la solution la plus rapide. Comme le langage que nous allons compiler (LSD010) ne permet pas de « déclaration a posteriori »3, c'est au moment de remplir l'AST lors de l'analyse syntaxique, à chaque fois que nous rencontrons une déclaration, que nous pouvons l'ajouter dans la table des symboles.

Table des matières Haut

Rechercher une valeur

Comme un des buts de notre table des symboles est de nous aider à retrouver rapidement un symbole, nous devrons implémenter une fonction getDeclaration(Char * symbolId) qui nous retourne le nœud qui correspond à notre symbole dans l'arbre syntaxique.

Comme nous avons vu que l'identificateur peut comporter d'autres informations que le nom (comme par exemple dans le cas d'identificateur de fonction), nous pouvons passer par une fonction qui nous calcule l'identificateur avant de le passer en paramètre, ou simplement passer un pointeur vers un nœud de l'AST qui contient toutes les informations nécessaires.
Nous avons donc cette signature : getDeclaration(AstNode symbolNode)

Comme nous utilisons le langage C pour réaliser notre compilateur, que ce langage ne permet d'indexer les tableaux que par des valeurs de type numérique entier, et que nous avons seulement une chaîne de caractères à chercher dans notre table des symboles, nous devrons effectuer une conversion entre la chaîne de caractères et l'entier. Si nous désirons un algorithme de conversion assez rapide, nous risquons fortement d'avoir pour un certain nombre de chaînes de caractères différentes une valeur numérique entière identique. Nous avons là un nouveau type de collisions, et nous devons à nouveau stocker une liste chaînée en tant que valeur associée à notre clé numérique. Lorsque nous cherchons une déclaration de symbole, nous devons parcourir les éléments de la liste et comparer la chaîne de caractères avec celle que nous cherchons pour déterminer si nous sommes en présence du bon nœud.

Table des matières Haut

Structure des données

Nous utiliserons le langage C pour réaliser notre compilateur7. La première idée est de générer un tableau dont la clé est le nom du symbole, et la valeur stockée est le nœud de l'AST, mais le langage C ne nous permet que des tableaux indexés par des clés de type int. En Java, par exemple, nous aurions pu utiliser une HashMap.

Hachage et identification des symboles

Table de hachage indexée par nom

Notre algorithme de hachage nous assure que le résultat produits par deux appels sur des identifiants identiques produira toujours le même résultat, mais pas deux appels sur des identifiants différents produira deux résultats différents. Ce qui revient à dire que nous pouvons avoir une même valeur de retour pour deux identifiants de symboles différents, ce qui provoque des « collisions » au niveau des clés de notre tableau.

Nous pouvons résoudre ce problème de collisions dans la table des symboles en utilisant une liste chaînée de nœuds comme valeur correspondant à un nom de symbole. De cette manière, nous ne perdons pas d'information.

Table des matières Haut

Alternatives pour les structures de données en C

Maintenant que nous avons soulevé certaines questions à propos de la gestion des symboles, nous pouvons penser à la structure de données de la table des symboles proprement dite. Nous avons un certain nombre d'alternatives qui s'offrent à nous, dont les deux décrites ci-dessous, qui diffèrent par l'importance que nous accordons aux identifiants des symboles ou à la portée courante.

Table des matières Haut

Une seule table pour les symboles

Table de piles de portées

Comme nous avons un algorithme de hachage à notre disposition pour les identifiants de symboles, nous pouvons utiliser un tableau unique dans lequel chaque cellule contient ce qui correspond à un identificateur de symbole. Nous utiliserons donc une structure particulière à stocker dans une cellule.
Comme nous avons vu que nous risquons d'avoir un même résultat pour le hashcode de plusieurs identificateurs différents, nous utiliserons comme structure à placer dans notre table des symboles une liste chaînée. Si le nom du premier élément de la liste ne correspond pas à notre symbole8, nous cherchons le nom de l'élément suivant. Normalement, si la taille du tableau est suffisante et l'algorithme de hachage assez dispersant, nous avons rarement besoin de chercher après un grand nombre d'éléments suivants.

Chaque élément de notre liste chaînée contiendra le nom du symbole et une pile qui correspond aux différentes portées de ce symbole9.

Nous retrouvons dans nos éléments de pile de portée l'identifiant de potrée, un pointeur vers le neœd qui contient la déclaration de notre symbole dans l'AST, et un pointeur vers la portée englobante.

Table des matières Haut

Une seule pile de portées

Pile de portées

Nous pouvons utiliser une pile de portées en tant que structure principale. Cette alternative nous évite de devoir calculer le hachage pour indexer notre tableau, et présente l'avantage d'être beaucoup plus économe en resources mémoire (Il n'est plus nécessaire de réserver l'espace mémoire pour le tableau). Cependant, elle est plus lente dans le cas des recherches, car nous devons à chaque fois parcourir la liste chaînée des déclarations pour la portée, et recommencer dans la portée englobante si nécessaire. Si la déclaration est dans le bas de la pile de portée, nous devons presque parcourir la totalité des déclarations.

Table des matières Haut

Optimiser la recherche

Les deux alternatives permettent de dépiler les déclarations quand nous quittons une portée pour retourner dans la portée englobante, mais nous pouvons

optimiser la recherche de différentes manières :

  • Déplacer en tête de notre liste chaînée la dernière déclaration lue. Par exemple, si nous lisons une variable, il est fort probable que nous ayons besoin d'y accéder à nouveau dans un laps de temps assez court.
  • Utiliser une pile externe d'une taille donnée, qui contient les pointeurs vers les dernières déclarations utilisées.

Version en cache

20/11/2024 18:54:06 Cette version de la page est en cache (à la date du 20/11/2024 18:54:06) afin d'accélérer le traitement. Vous pouvez activer le mode utilisateur dans le menu en haut pour afficher la dernère version de la page.

Document créé le 11/07/2010, dernière modification le 28/10/2018
Source du document imprimé : https://www.gaudry.be/langages-table-des-symboles.html

L'infobrol est un site personnel dont le contenu n'engage que moi. Le texte est mis à disposition sous licence CreativeCommons(BY-NC-SA). Plus d'info sur les conditions d'utilisation et sur l'auteur.

Notes
  1.  Variables et fonctions : Nous parlerons de symboles car cela s'applique aux variables utant qu'aux fonctions.

  2.  déclaration anticipative : correspond à “backward declaration” en anglais

  3. a,b déclaration a posteriori : correspond à “forward declaration” en anglais

  4. a,b,c,d,e,f… 1 en plus… Abstract Syntaxic Tree : correspond à « arbre syntaxique abstrait » en français

  5. a,b,c,d,e,f AST : “Abstract Syntaxic Tree” (en français, « arbre syntaxique abstrait »)

  6.  LSD010 : Langage Simple et Didactique Il existe une un certain nombre d'interprétations de l'acronyme LSD (Langage Symbolique Didactique, Langage Sans Difficulté, Langage Simple et Didactique). LSD010 est la version 2010 de la suite LSD80, LSD_02, LSD03, LSD04, LSD05, LSD06, LSD07, LSD08, et LSD09.

  7.  Langage du compilateur : Nous aurions pu choisir un autre langage pour réaliser notre code interne au sein du compilateur, comme par exemple Java.

  8.  Identificateur vs nom : Nous parlons de nom de symbole et pas d'identificateur, car l'identificateur peut comporter d'autres informations que le nom, comme par exemple dans le cas d'identificateur de fonction.

  9.  Stockage du nom de symbole : En réalité, comme nous avons déjà toutes les informations nécessaires dans le nœud de notre AST, nous ne mémorisons pas le nom dans l'élément lui-même, mais nous le lisons dans le premier élément de la pile de portée, qui est un pointeur vers le nœud de la déclaration.

Table des matières Haut

Références

  1. livre Langue du document :fr IHDCB332 - Théorie des langages : Syntaxe et sémantique : PY Schobbens, Syntaxe et sémantique (January 2010)
  2. livre Langue du document :fr Compilateurs : Dick Grune, Henry E. Bal, Ceriel J.H. Jacobs, Koen G. Langendoen, Cours et exercices corrigés
  3. livre Langue du document :fr Compilateurs : A. Aho, M. Lam, R. Sethi, J. Ulman, Principes; techniques et outils

Ces références et liens indiquent des documents consultés lors de la rédaction de cette page, ou qui peuvent apporter un complément d'information, mais les auteurs de ces sources ne peuvent être tenus responsables du contenu de cette page.
L'auteur de ce site est seul responsable de la manière dont sont présentés ici les différents concepts, et des libertés qui sont prises avec les ouvrages de référence. N'oubliez pas que vous devez croiser les informations de sources multiples afin de diminuer les risques d'erreurs.

Table des matières Haut