Comment créer un interpréteur de langage simple ?

Un interpréteur est un programme qui lit, analyse et exécute du code écrit dans un langage spécifique sans le compiler au préalable. Créer un interpréteur simple est une excellente manière de comprendre les concepts fondamentaux de la programmation, tels que l’analyse syntaxique, l’évaluation d’expressions et la gestion de l’environnement d’exécution.

Dans cet article, nous allons voir les étapes essentielles pour construire un interpréteur minimaliste pour un langage simple, en Python.


1. Comprendre les composants d’un interpréteur

Un interpréteur se compose généralement de plusieurs parties :

1️⃣ Lexer (Analyse lexicale) : Convertit le code source en une liste de tokens (unités lexicales).
2️⃣ Parser (Analyse syntaxique) : Transforme la liste de tokens en un arbre syntaxique abstrait (AST).
3️⃣ Interpréteur (Évaluation et exécution) : Parcourt l’AST et exécute les instructions.

Exemple de code source simple dans notre mini-langage :

txtCopierModifierx = 5 + 3;
print(x);

L’interpréteur doit comprendre :

  • Que x = 5 + 3; est une affectation de variable.
  • Que print(x); affiche la valeur de x.

2. Étape 1 : Création du Lexer

Le lexer découpe le texte en tokens. Voici les types de tokens que nous allons gérer :

TokenExemple
Identificateurx, print
Nombre5, 3
Opérateur+, -, *, /
Symbole=, ;, (, )

Implémentation du Lexer en Python

pythonCopierModifierimport re

# Définition des types de tokens
TOKEN_TYPES = [
    ("NUMBER", r"\d+"),
    ("IDENTIFIER", r"[a-zA-Z_][a-zA-Z0-9_]*"),
    ("OPERATOR", r"[+\-*/=]"),
    ("SYMBOL", r"[();]"),
    ("WHITESPACE", r"\s+"),
]

class Lexer:
    def __init__(self, source_code):
        self.source_code = source_code
        self.tokens = []
    
    def tokenize(self):
        position = 0
        while position < len(self.source_code):
            match_found = False
            for token_type, pattern in TOKEN_TYPES:
                regex = re.compile(pattern)
                match = regex.match(self.source_code, position)
                if match:
                    text = match.group(0)
                    if token_type != "WHITESPACE":  # On ignore les espaces
                        self.tokens.append((token_type, text))
                    position += len(text)
                    match_found = True
                    break
            if not match_found:
                raise SyntaxError(f"Caractère inconnu : {self.source_code[position]}")
        return self.tokens

# Test du Lexer
lexer = Lexer("x = 5 + 3; print(x);")
tokens = lexer.tokenize()
print(tokens)  # Output attendu : [('IDENTIFIER', 'x'), ('OPERATOR', '='), ('NUMBER', '5'), ('OPERATOR', '+'), ('NUMBER', '3'), ('SYMBOL', ';'), ('IDENTIFIER', 'print'), ('SYMBOL', '('), ('IDENTIFIER', 'x'), ('SYMBOL', ')'), ('SYMBOL', ';')]

3. Étape 2 : Création du Parser (AST)

Le parser transforme la liste de tokens en un arbre syntaxique abstrait (AST). Cet arbre représente la structure logique du programme.

Nous allons créer un AST simple avec trois types de nœuds :

  • NumberNode : Représente un nombre.
  • BinaryOpNode : Représente une opération mathématique (+, -, *, /).
  • AssignmentNode : Représente l’affectation (x = ...).

Implémentation du Parser

pythonCopierModifierclass NumberNode:
    def __init__(self, value):
        self.value = value

class BinaryOpNode:
    def __init__(self, left, operator, right):
        self.left = left
        self.operator = operator
        self.right = right

class AssignmentNode:
    def __init__(self, identifier, expression):
        self.identifier = identifier
        self.expression = expression

class Parser:
    def __init__(self, tokens):
        self.tokens = tokens
        self.position = 0

    def eat(self, expected_type):
        if self.position < len(self.tokens) and self.tokens[self.position][0] == expected_type:
            self.position += 1
            return self.tokens[self.position - 1]
        else:
            raise SyntaxError(f"Token attendu : {expected_type}")

    def parse_expression(self):
        left = self.parse_term()
        while self.position < len(self.tokens) and self.tokens[self.position][1] in "+-":
            operator = self.eat("OPERATOR")[1]
            right = self.parse_term()
            left = BinaryOpNode(left, operator, right)
        return left

    def parse_term(self):
        token = self.eat("NUMBER")
        return NumberNode(int(token[1]))

    def parse_statement(self):
        if self.tokens[self.position][0] == "IDENTIFIER":
            identifier = self.eat("IDENTIFIER")[1]
            self.eat("OPERATOR")  # Le "="
            expression = self.parse_expression()
            self.eat("SYMBOL")  # Le ";"
            return AssignmentNode(identifier, expression)

    def parse(self):
        return self.parse_statement()

# Test du Parser
parser = Parser(tokens)
ast = parser.parse()
print(ast)

4. Étape 3 : Création de l’Interpréteur

L’interpréteur exécute l’AST. Il maintient une table des variables et évalue les expressions.

Implémentation de l’Interpréteur

pythonCopierModifierclass Interpreter:
    def __init__(self):
        self.variables = {}

    def evaluate(self, node):
        if isinstance(node, NumberNode):
            return node.value
        elif isinstance(node, BinaryOpNode):
            left_value = self.evaluate(node.left)
            right_value = self.evaluate(node.right)
            if node.operator == '+':
                return left_value + right_value
            elif node.operator == '-':
                return left_value - right_value
        elif isinstance(node, AssignmentNode):
            value = self.evaluate(node.expression)
            self.variables[node.identifier] = value
            return value

# Exécution de l'interpréteur
interpreter = Interpreter()
result = interpreter.evaluate(ast)
print(interpreter.variables)  # Doit afficher {'x': 8}

5. Conclusion et améliorations possibles

Nous avons vu comment construire un interpréteur minimaliste en trois étapes :
✅ Lexer : Convertit le texte en tokens.
✅ Parser : Transforme les tokens en un AST.
✅ Interpréteur : Exécute le programme et stocke les variables.

Améliorations possibles

🚀 Ajouter les opérations * et /.
🚀 Implémenter des fonctions et des boucles.
🚀 Ajouter une instruction print pour afficher des valeurs.

Cet exercice permet de mieux comprendre le fonctionnement des langages de programmation. Si vous souhaitez aller plus loin, explorez l’implémentation d’un langage complet avec gestion des types et interprétation avancée !

carle
carle