GCC (General Compiler Concept) course

Damien Lejosne

2026 - 03 - 03

Introduction

Parsing

Tokenizer (Lexer)

Automates finis (déterministes) et langages réguliers.

Parser

Grammaires hors contextes.

Parser descendant naïf

Peut avoir une complexité exponentielle + non déterministe en cas d’ambiguités.

On va donc essayer de faire du parsing prédictif.

LL(k)

Il s’agit d’un parser descendant.

On regarde k caractères en avance ce qu’il y a en entrée, de sorte à savoir quelle règle appliquer.

Quelques définitions :

Un symbole non terminal est nullable si

On appelle FIRST(y) l’ensemble des terminaux par lesquels commencent y.

On appelle FOLLOW(y) l’ensemble des terminaux pouvant suivre y.

On peut alors déterminer facilement par des régles récursives les nullables, FOLLOW et FIRST de chaque non terminal.

On construit alors une table de parsing dans laquelle on place, pour toute paire (NT, t)

Limites :

LR(k) (left to right parse, rightmost derivation)

Notations : T,t = terminal, NT = Non Terminal

Plus puissant.

Se base sur un automate à pile.

Il s’agit d’une analyse montante.

LR(0)

Parsing en 3 étapes :

  1. On commence par créer un automate, qui va nous permettre de créer une table de parsing :

On pose un symbole spécial “.” marquant l’avancement dans le parsing d’une règle.

Exemple : Exemple automate LR(0)

  1. À partir de cet automate, on crée une table de parsing :

On place les non terminaux au début en indice de colonnes, les termninaux en fin en indice de colonne. On place les état de l’automate précédemment créé en indice des lignes.

Dans une case, on met :

Exemple : Exemple table parsing LR(0)

  1. Parsing

On a une pile, sur laquelle on garde les symboles (terminaux ou non) et les états dans lequel était l’automate quand le symbole a été empilé.

On commence dans l’état \(1\).

Si on est dans l’état \(i\) et qu’on lit le symbole terminal \(t\), on regarde le contenu de la case de la table précédemment créée :

Exemple: Exemple parsing LR(0)

LR(1)

???

SLR

On supprime les transitions \(r_i\) de la table si le terminal de la colonne n’est pas contenu dans \(FIRST(NT_i)\)

Analyse sémantique

Objectif : donner du sens à un programme en transformant l’entrée en représentation intermédiaire utilisable par le middle-end, analysé les erreurs de typages…

Approche 1 : Grammaire attribuée

Une grammaire attribuée est une grammaire de programme à laquelle on a rajouté :

On analyse ainsi un code parser en retournant un résultat à la racine de l’arbre.

Exemple : Nombre binaire :

Exemple grammaire attribuée addition

/!\ : Ordre d’évaluation important !

Différentes méthodes pour cette analyse :

Si il y a un cycle… :

Approche 2 : Construction ad hoc

Similaire aux grammaires attribuées (avec l’info que dans un sens), on associe des actions à chaque production de la grammaire.

Exemple : construction d’un AST.

AST (Abstract Syntax Tree) : Par rapport à un CST (concrete syntax tree, abre obtenu en créant l’abre de dérivation en appliquant la grammaire), on supprime les non terminaux, on met les terminaux “discriminants” / “importants”.

Design Pattern : Visiteurs

Le design pattern visiteurs correspond au parcours d’un objet en appliquant un traitement spécifique à chaque élément.

RQ: Par rapport aux approches précédentes, ça revient à appliquer les actions après que l’arbre n’ait été construit.

Représentation intermédiaire (IR)

Une représentation intérmediaire est la représentation interne du compilateur.

Elle doit : - Être efficace - Abstraite (indépendante de l’architecture)

Il en existe de nombreuses, avec leurs avantages et inconvénients, certains compilateurs en utilisent plusieurs. (CompCert : 20 IR !)

Framework MLIR (Medium Level IR)

Différents niveaux d’IR : Déjà vus

Graphe de flot de contrôle (CFG) :

Bloc de base : Un point d’entrée, et un de sortie.

Souvent représenté par AST ou DAG.

Construction du CFG :

Par visiteurs : On récupère pour les enfants si c’est un nouveau bloc ou non : - Si s’en est un, il a un point d’entrée et de sortie, et on s’y connecte pour créer le flot. - Sinon, on l’ajoute aux statements du bloc.

Code du visitBlockStatement

Graphe de dépendance

Machine à pile

Code 3 adresses

Forme SSA (Single Static Assignment)

Propriété de \(\Phi\)

\(\Phi\) a une arité égale au nombre de prédecesseur. On choisit l’opérande selon le bloc éxecuté précédemment.

Construction

Idée :

Pour ce faire, il faut assigner un nom unique aux valeurs d’un bloc de base :

Local Value Numbering : Assignation d’un nom unique aux valeurs d’un bloc de base.

On utilise un tableau currentDef qui associe les valeurs SSA dans un bloc donné.

Puis, il faut faire de même avec les \(\Phi\), c’est ce qu’on appelle le Global Value Numbering.

Ça nous donne l’algo suivant :

Code du readVaraibleRec

RQ :

CF : Braun & al, “Simple and efficient construciton of SSA Form”, 2013

PB : On veut construire la forme SSA en même temps que le CFG.

\(\rightarrow\) sur un CFG partiel, issu de la transformation AST \(\rightarrow\) IR.

Optimisations

Objectifs

Quelques exemples d’équations dataflow

Liveness Analysis

On dit qu’une variable est :

RQ :

Reaching definitions

Objectif : Savoir si une assignation peut impacter sa valeur à un autre endroit du programme.

Une variable est :

Ici, on ne s’intéresse qu’aux non ambigues.

Pour un statement \(s\) de la forme d1 : t := a + b, on dit que :

On appelle aussi ces ensembles gen et kill.

Available expressions

Objectif : Savoir si une expression a déjà été calculée.

Un statement qui calcule \(a + b\) génère \(\{ a + b \}\) mais tout statement qui modifie la valeur de \(a\) ou \(b\) la détruit.

Work list algorithm

C’est l’algo le plus efficace pour résoudre les équations dataflow.

Transformations

Élimination de sous expressions communes

Quand s : t := x + y est available, on calcule le reaching expression sur le statement, on assigne ce statement à une variable temporaire et on remplace le statement par cette variable temporaire.

MAIN WEBPAGE