Partielo | Créer ta fiche de révision en ligne rapidement

REVISION C

BUT DU PROJET

***~ Le but etait de creer un compilateur complet en C pour un mini-langage inventé qui s'appele

simplCT. Ce langage permet d'ecrire des programmes simples : on peut y déclarer des variables

entieres, faire des calculs, des conditions if, des boucles while, et afficher des resultats.


  • Un compilateur, c'est un programme qui lit du code source (ecrit par un humain) et le transforme en quelque chose qu'une machine peut executer. Ici à l'aide de tokens on produit du bytecode — une serie d'instructions simplifiees interpretees par une machine virtuelle fournie.


P1 - Lexer (code source C -> tokens)

  • ***C'est quoi un lexer ?

Le lexer lit le fichier source caractere par caractere et le decoupe en tokens. C'est comme reconnaitre les mots d'une phrase avant de la comprendre, c'est le travail du lexer

Exemple : Code source : let x = 5 + 3;

Traduction en token par le lexer → TOKEN_LET · TOKEN_ID("x") · TOKEN_ASSIGN · TOKEN_INT(5) · TOKEN_PLUS · TOKEN_INT(3) · TOKEN_SEMICOLON


***Fonctions ajoutées par nous à connaitre dans lexer.c :


peek_char : regarder le caractere courant dans le code source sans avancer

peek_next_char : regarder le caractère suivant (position courante i +1) sans le consommer/sans avancer (utile pour == etc)

advance_pos : avancer d'un caractere (& compter les lignes sur les \n), et le retourner

check_keyword() : prend une chaîne de caractères & vérifie si elle correspond à un mot-clé du langage simplCT. Si c’est le cas, elle retourne le type de token correspondant

scan_identifier : lire un mot complet puis verifier si c'est un mot-cle (let, if, while...) via check_keyword puis crée le token correspondant si oui/ ou un identifiant (variable créée par l'utilisateur)

scan_number : lire une suite de chiffres et produire un TOKEN_INT

scan_string : lire tout ce qui est entre guillemets, donc ca lit une chaine de caractère (STRING), et crée un token_string si les guillemets se ferme sinon crée un token_error

tokenize : boucle principale qui gère tout et retourne la liste chainée de tokens, choisis les bonnes fonctions de scan selon ce qui est rencontré dans le code source. Une fois fini, elle genere un token_eof

P2 - Compilateur (génération en bytecode)


  • ***C'est quoi concretement le compilateur ?

Le compilateur prend la liste de tokens produite par le lexer et genere une suite

d'instructions 'bytecode'. La machine virtuelle (fournie par le prof == vm.c) les execute ensuite une par une avec une pile et une table de variables.


** Fonctions à connaitre :

checkregarde si le token courant est du type voulu, sans avancer.

advance — passe au token suivant (et met l'ancien dans previous)

matchcombine check + advance : si le token courant est du bon type, il avance et retourne 1. Sinon, ne fait rien et retourne 0. C'est le cœur de la lecture de tokens : "si tu vois X, consomme-le".

expectcomme match mais obligatoire : si le token n'est pas du bon type, affiche une erreur et active le flag had_error. (Utilisé pour les éléments syntaxiques nécessaires (ex: le ; après chaque instruction).)

compile_primarycelui qui emettra les instructions ; cas initial : un entier, un string, un identifiant (variable), ou une expression entre parenthèses. Génère les instructions

compile_factor → (appelle compile_primary), puis gère * et /

compile_term → (appelle compile_factor), puis gère + et -. Plus basse priorité que *//, donc 2 + 3 * 4 sera bien calculé dans le bon ordre.

compile_comparisongère <, >, <=, >=.

compile_equality gère == et !=, la priorité la plus basse.

compile_expression → tout commence par ici (point d'entrée): appelle compile_equality, qui appellera d'autre fonctions jusqua trouver la bonne

compile_let_statementgère let x = expr;. Il attend un identifiant, puis =, compile l'expression

compile_if_statement gère if (condition) { bloc }. Compile la condition

compile_blockgère { ... } : attend {, compile des instructions jusqu'à }.

compile_statement — aiguillage général : selon le token courant, délègue à la bonne fonction de compilation. Gère aussi l'assignation x = expr; (sans let).

compile fonction principale : initialise le compilateur, compile toutes les instructions jusqu'à TOKEN_EOF, et retourne le bytecode ou NULL si erreur



*Fonctionnement :


tout d'abord il faut respecter l'ordre des priorités mathématiques, pour cela ; Chaque fonction gère UN niveau de priorité.

Par exemple :

  • compile_factor s’occupe de * et /
  • compile_term s’occupe de + et -
  • Et comme compile_term appelle avant compile_factor, les multiplications sont calculées avant les additions automatiquement.

Les fonctions imbriquées servent à respecter les priorités des opérateurs.

Le backpatching (pour fonctions compile if/while) sert à corriger les sauts une fois qu’on connaît leur destination. (on dirige le saut vers 0 et des qu'on connait son adresse finale on vient la modifier afin de jump vers la bonne adresse)

  • compile_statement choisit quelle partie du code compiler selon le token actuel. (exemple token while -> elle compile du while)




P3 - Annexe & Soutenance

Le main.c orchestre les 4 etapes et les affiche : code source (en C) → (transformation par le lexer) tokens → (tokens lu puis compiler pour la machine ) bytecode → execution (vm : machine virtuelle). Deux programmes de demo codés en simplCT ont ete préparés :


fibonacci.simpl — calcule les 10 premiers termes de la suite par decalage iteratif (tmp = a + b, puis a = b, b = tmp)


somme_chiffres.simpl — extrait les chiffres d'un entier sans modulo via tmp - (tmp / 10) * 10, pour n=493 → resultat 16


à noter pour vm : PUSH: empile, POP: depile, EXECUTE : boucle principal lisant et executant les instructions, JUMP: saute vers une adresse



Difficultées rencontrées :


  • backpatching : Au debut on voulait calculer l'adresse de saut avant de compiler le bloc, ce qui est impossible. Il a fallu comprendre qu'il faut d'abord emettre un placeholder (saut temporaire, vers 0 par exemple) puis, compiler le bloc entier, et seulement ensuite corriger l'adresse.
  • distribution du travail à cause des disponibilitées qui pouvaient différer
  • la gestion des priorités d'operateurs ( cascade de priorités avec les appels suffit) (compileterm et compile_factor)



Questions à connaitre :


-> C'est quoi un token ? : Un jeton issu du code source : ca peut etre un mot-cle, un identifiant, un operateur, un nombre. Le lexer les produit, le compilateur les consomme.


-> Comment fonctionne la pile de la VM ? : chaque instruction pousse des valeurs dessus, les opérations récupèrent les dernières valeurs et les dépile, calculent le résultat puis remettent ce résultat sur la pile ou le repousse.


-> Pourquoi une liste chainee pour les tokens ? Parce qu'on ne connait pas a l'avance le nombre de tokens. La liste chainee permet d'ajouter dynamiquement sans reallocation.


-> Qu'est-ce que le backpatching ? Technique de correction differee : on emet un saut avec une adresse provisoire, puis on la corrige une fois qu'on connait l'adresse reelle apres avoir compile le bloc.


-> Pourquoi la cascade de priorite ? Pour respecter les regles mathematiques sans code supplementaire — la structure des appels suffit. compile_factor est appele avant compile_term, donc * est traite avant +.


Limites (pas à connaitre) :

  • pas de else
  • identifiants avec chiffres non supportés



REVISION C

BUT DU PROJET

***~ Le but etait de creer un compilateur complet en C pour un mini-langage inventé qui s'appele

simplCT. Ce langage permet d'ecrire des programmes simples : on peut y déclarer des variables

entieres, faire des calculs, des conditions if, des boucles while, et afficher des resultats.


  • Un compilateur, c'est un programme qui lit du code source (ecrit par un humain) et le transforme en quelque chose qu'une machine peut executer. Ici à l'aide de tokens on produit du bytecode — une serie d'instructions simplifiees interpretees par une machine virtuelle fournie.


P1 - Lexer (code source C -> tokens)

  • ***C'est quoi un lexer ?

Le lexer lit le fichier source caractere par caractere et le decoupe en tokens. C'est comme reconnaitre les mots d'une phrase avant de la comprendre, c'est le travail du lexer

Exemple : Code source : let x = 5 + 3;

Traduction en token par le lexer → TOKEN_LET · TOKEN_ID("x") · TOKEN_ASSIGN · TOKEN_INT(5) · TOKEN_PLUS · TOKEN_INT(3) · TOKEN_SEMICOLON


***Fonctions ajoutées par nous à connaitre dans lexer.c :


peek_char : regarder le caractere courant dans le code source sans avancer

peek_next_char : regarder le caractère suivant (position courante i +1) sans le consommer/sans avancer (utile pour == etc)

advance_pos : avancer d'un caractere (& compter les lignes sur les \n), et le retourner

check_keyword() : prend une chaîne de caractères & vérifie si elle correspond à un mot-clé du langage simplCT. Si c’est le cas, elle retourne le type de token correspondant

scan_identifier : lire un mot complet puis verifier si c'est un mot-cle (let, if, while...) via check_keyword puis crée le token correspondant si oui/ ou un identifiant (variable créée par l'utilisateur)

scan_number : lire une suite de chiffres et produire un TOKEN_INT

scan_string : lire tout ce qui est entre guillemets, donc ca lit une chaine de caractère (STRING), et crée un token_string si les guillemets se ferme sinon crée un token_error

tokenize : boucle principale qui gère tout et retourne la liste chainée de tokens, choisis les bonnes fonctions de scan selon ce qui est rencontré dans le code source. Une fois fini, elle genere un token_eof

P2 - Compilateur (génération en bytecode)


  • ***C'est quoi concretement le compilateur ?

Le compilateur prend la liste de tokens produite par le lexer et genere une suite

d'instructions 'bytecode'. La machine virtuelle (fournie par le prof == vm.c) les execute ensuite une par une avec une pile et une table de variables.


** Fonctions à connaitre :

checkregarde si le token courant est du type voulu, sans avancer.

advance — passe au token suivant (et met l'ancien dans previous)

matchcombine check + advance : si le token courant est du bon type, il avance et retourne 1. Sinon, ne fait rien et retourne 0. C'est le cœur de la lecture de tokens : "si tu vois X, consomme-le".

expectcomme match mais obligatoire : si le token n'est pas du bon type, affiche une erreur et active le flag had_error. (Utilisé pour les éléments syntaxiques nécessaires (ex: le ; après chaque instruction).)

compile_primarycelui qui emettra les instructions ; cas initial : un entier, un string, un identifiant (variable), ou une expression entre parenthèses. Génère les instructions

compile_factor → (appelle compile_primary), puis gère * et /

compile_term → (appelle compile_factor), puis gère + et -. Plus basse priorité que *//, donc 2 + 3 * 4 sera bien calculé dans le bon ordre.

compile_comparisongère <, >, <=, >=.

compile_equality gère == et !=, la priorité la plus basse.

compile_expression → tout commence par ici (point d'entrée): appelle compile_equality, qui appellera d'autre fonctions jusqua trouver la bonne

compile_let_statementgère let x = expr;. Il attend un identifiant, puis =, compile l'expression

compile_if_statement gère if (condition) { bloc }. Compile la condition

compile_blockgère { ... } : attend {, compile des instructions jusqu'à }.

compile_statement — aiguillage général : selon le token courant, délègue à la bonne fonction de compilation. Gère aussi l'assignation x = expr; (sans let).

compile fonction principale : initialise le compilateur, compile toutes les instructions jusqu'à TOKEN_EOF, et retourne le bytecode ou NULL si erreur



*Fonctionnement :


tout d'abord il faut respecter l'ordre des priorités mathématiques, pour cela ; Chaque fonction gère UN niveau de priorité.

Par exemple :

  • compile_factor s’occupe de * et /
  • compile_term s’occupe de + et -
  • Et comme compile_term appelle avant compile_factor, les multiplications sont calculées avant les additions automatiquement.

Les fonctions imbriquées servent à respecter les priorités des opérateurs.

Le backpatching (pour fonctions compile if/while) sert à corriger les sauts une fois qu’on connaît leur destination. (on dirige le saut vers 0 et des qu'on connait son adresse finale on vient la modifier afin de jump vers la bonne adresse)

  • compile_statement choisit quelle partie du code compiler selon le token actuel. (exemple token while -> elle compile du while)




P3 - Annexe & Soutenance

Le main.c orchestre les 4 etapes et les affiche : code source (en C) → (transformation par le lexer) tokens → (tokens lu puis compiler pour la machine ) bytecode → execution (vm : machine virtuelle). Deux programmes de demo codés en simplCT ont ete préparés :


fibonacci.simpl — calcule les 10 premiers termes de la suite par decalage iteratif (tmp = a + b, puis a = b, b = tmp)


somme_chiffres.simpl — extrait les chiffres d'un entier sans modulo via tmp - (tmp / 10) * 10, pour n=493 → resultat 16


à noter pour vm : PUSH: empile, POP: depile, EXECUTE : boucle principal lisant et executant les instructions, JUMP: saute vers une adresse



Difficultées rencontrées :


  • backpatching : Au debut on voulait calculer l'adresse de saut avant de compiler le bloc, ce qui est impossible. Il a fallu comprendre qu'il faut d'abord emettre un placeholder (saut temporaire, vers 0 par exemple) puis, compiler le bloc entier, et seulement ensuite corriger l'adresse.
  • distribution du travail à cause des disponibilitées qui pouvaient différer
  • la gestion des priorités d'operateurs ( cascade de priorités avec les appels suffit) (compileterm et compile_factor)



Questions à connaitre :


-> C'est quoi un token ? : Un jeton issu du code source : ca peut etre un mot-cle, un identifiant, un operateur, un nombre. Le lexer les produit, le compilateur les consomme.


-> Comment fonctionne la pile de la VM ? : chaque instruction pousse des valeurs dessus, les opérations récupèrent les dernières valeurs et les dépile, calculent le résultat puis remettent ce résultat sur la pile ou le repousse.


-> Pourquoi une liste chainee pour les tokens ? Parce qu'on ne connait pas a l'avance le nombre de tokens. La liste chainee permet d'ajouter dynamiquement sans reallocation.


-> Qu'est-ce que le backpatching ? Technique de correction differee : on emet un saut avec une adresse provisoire, puis on la corrige une fois qu'on connait l'adresse reelle apres avoir compile le bloc.


-> Pourquoi la cascade de priorite ? Pour respecter les regles mathematiques sans code supplementaire — la structure des appels suffit. compile_factor est appele avant compile_term, donc * est traite avant +.


Limites (pas à connaitre) :

  • pas de else
  • identifiants avec chiffres non supportés