Analyseur grammatical
Principe
Maintenant que vous disposez d'un analyseur lexical capable de découper le code source en lexèmes, il est tant d'en extraire la structure. Comme pour les langages naturels, le concept de grammaire est utilisé pour décrire la structure du langage.
Voici la définition de la grammaire du langage Kale :
KaleFile ::= Assignment* EOF
Assignment ::= IDENT ASSIGN Value
Value ::= MulDivValue ( epsilon | (+ | -) MulDivValue )*
MulDivValue ::= AtomValue ( epsilon | (* | /) AtomValue )*
AtomValue ::= INTEGER | IDENT | STRING
Les éléments de la grammaire entièrement en majuscule (ie. INTEGER
) correspondent aux lexèmes et sont appelés les terminaux.
Les autres éléments sont les non-terminaux et représentent le résultat des règles qui structurent le langage.
La grammaire se lit de cette façon :
- la règle
Assignment ::= IDENT ASSIGN Value
signifie que la séquence formée deIDENT
,ASSIGN
etValue
peut se réduire àAssignment
; |
indique une alternative, ie.INTEGER
ouIDENT
ouSTRING
peuvent se réduire àAtomValue
. Ceci est équivalent à avoir écrit séparément les règles suivantes :
AtomValue ::= INTEGER
AtomValue ::= IDENT
AtomValue ::= STRING
*
indique que l'élément précédent peut être répété 0 ou plusieurs fois ;- les parenthèses servent à préciser les priorités des opérations ;
epsilon
représente la séquence vide c.-à-d. l'absence d'éléments.
La structure résultant de l'analyse du language est définie dans le fichier ast.ts
: chaque interface correspond
à une règle de la grammaire .
Il faut maintenant définir la machine capable d'extraire la structure à partir de cette grammaire : le parser. Les parsers sont classés en fonction de la complexité du langage qu'ils sont capable de reconnaître. Les parsers les plus simples à implémenter sont ceux dit LL(1).
Attention ce type de parser ne permet pas d'être utilisé avec n'importe qu'elle grammaire :
elle ne doit pas contenir de règles récursives à gauche et être factorisé.
Mais pas d'inquiétude, cette grammaire a été écrite pour être de type LL(1)
(d'où les règles Value
et MulDivValue
écrites d'une façon pas très naturelle).
Implémentation
Voici la structure générale du parser :
class Parser {
private token: Token;
constructor(private lexer: Lexer) {
this.token = this.scanToken()
}
parseFile(): KaleFile {
throw new Error("TODO: not complete");
}
private scanToken() {
this.token = this.lexer.nextToken();
return this.token;
}
private test(kind: TokenKind) {
if (this.token.kind == kind) {
this.scanToken();
return true;
}
return false;
}
private expect(kind: TokenKind) {
if (this.token.kind != kind) {
throw new Error(`Expecting token ${kind}, got ${this.token.kind}`);
}
this.scanToken();
}
}
Les parsers de type LL(1) peuvent être écrit sous la forme d'un automate procédurale, c.-à-d. que chaque
non-terminal de la grammaire est décrit par une méthode du parser. Par exemple la règle Assignment ::= IDENT ASSING Value
va se traduire en :
class Parser {
private parseAssignment(): Assignment {
const variable = this.parseIdent();
this.expect(TokenKind.ASSIGN);
const value = this.parseValue();
return { variable, value };
}
}
Si lors de l'analyse d'un non-terminal peut se poursuivre par plusieurs types de lexèmes : ils seront tout à tour tester avec la fonction test
.
Les répétitions dans la grammaire représentées par *
deviennent des boucles dans le parser.
Résultat
Une fois que l'ensemble des tests de l'étape step-1.2
passent au vert (npm test
), il est possible de tester
le résultat du parser sur les fichiers d'exemples (après compilation avec npm run build
) :
./bin/kale-interpreter examples/simple_example.kl
Le programme doit maintenant afficher l'arbre de syntaxe au format Json.