Oui il y a des images ;)
# build and execute
dune build && dune exec ./kawai.exe -- <args>
dune build && ./kawai.exe <args>
# run
./kawai.exe <file1> [--help | --show-source | -s]
# exemples :
./kawai.exe ./tests/var.kwa
dune build && ./kawai.exe -sAuteurs:
- Arthur SAILLANT (LDD3IM)
- Rayan LALAOUI (LDD3IM)
Nous avons abordé le projet de manière incrémentale, en établissant un ordre de mise en œuvre des fonctionnalités basé sur leur complexité et la simplicité des tests associés. Après avoir mis en place un framework permettant de choisir et de gérer les tests de manière efficace, notre objectif initial était d'afficher les tokens (en couleur !) afin de faciliter le débogage. Une fois cette étape terminée, nous avons poursuivi avec la phase de parsing, puis nous avons implémenté l'interpréteur capable de traiter tous les tests fournis dans le cadre du projet. Ce n'est qu'ensuite que nous avons développé le typechecker.
Au début, la répartition des tâches s'est faite de manière assez fluide : Arthur s'est principalement chargé de l'affichage des tokens, du parsing et de l'interpréteur, tandis que Rayan a pris en charge l'implémentation du typechecker, assurant ainsi la vérification des types pour tous les tests de base.
Ensuite, les fonctionnalités supplémentaires ont été implémentées individuellement, chaque membre prenant en charge l'ajout de fonctionnalités spécifiques de son côté. Cela a impliqué des modifications dans plusieurs fichiers du projet, ce qui a occasionné quelques difficultés lors des merges Git. Bien que ces conflits aient parfois causé des confusions, ils ont été résolus grâce au sang-froid de Rayan, qui a su gérer la situation avec calme et efficacité.
Nous avons rencontré plusieurs difficultés au cours du projet, notamment en ce qui concerne l'utilisation de Git et la gestion des conflits. Mais, la difficulté majeure a été le parsing, surtout en ce qui concerne les fonctionnalités supplémentaires, qui introduisaient des aspects de syntaxe assez complexes.
Pour rendre notre langage compatible avec une analyse LR(1), nous avons dû modifier certaines parties de la syntaxe "habituelle" (inspirée de Java). Par exemple, pour le typecast, nous avons choisi une syntaxe inspirée de Rust, passant de la forme (type) expr à expr as type. Cette modification a permis une meilleure intégration avec le parseur, mais a ajouté de la complexité à la syntaxe.
Un autre défi est survenu avec la gestion des types génériques. Nous avons décidé d'introduire le mot-clé generic pour que Menhir puisse choisir plus facilement la règle appropriée, ce qui rend la syntaxe malheureusement verbeuse.
Enfin, l'ajout de messages d'erreur détaillés a constitué un casse-tête. Menhir ne dispose pas d'une documentation très complète, ce qui a rendu difficile la compréhension de son fonctionnement interne. Cela a rendu l'implémentation des messages d'erreur plus complexe que prévu.
Nous avons ajouté un test pour chaque fonctionnalité implémentée, y compris des tests conçus pour échouer. Ils ont permis de vérifier le bon fonctionnement de la syntaxe et du type checker en simulant des erreurs intentionnelles. Ces tests ont validé la capacité du compilateur à réagir correctement aux erreurs et de s'assurer que les messages d'erreur étaient appropriés et clairs.
- Les fonctionnalités de base
- Champs immuables (final.kwa)
- Visibilités (encapsulation.kwa)
- Déclarations en série (multivar.kwa)
- Déclaration avec valeur initiale (multivarinit.kwa)
- Champs statiques
- Test de type (
instance of) (instanceof.kwa) - Transtypage (cast) (rayan.kwa)
- Super (super.kwa)
- Tableaux (tab.kwa et objtab.kwa )
- Égalité structurelle (
===&=/=) (structeq.kwa) - Classes et méthodes abstraites (on a des interfaces à default)
- Surcharge statique
- Déclarations simplifiées (enlever var, method)
- "Missing semicolon" (missingsemi.kwa)
- "Did you mean '...'?" (didyou.kwa je recommande ce test)
- Le processus ne peut pas aboutir en raison d'un problème technique
- Syntaxe abstraite typée (
annotetexpr_)
- déclaration et initialisation de variable en tant qu'instruction (multivarinit.kwa)
- Portée & shadowing (shadowing.kwa)
- Classes génériques (generictype.kwa)
- Interfaces (avec ou sans implémentation par défaut) (interface_simple.kwa & interface_default.kwa)
- des messages d'erreurs plus explicite (voir l'handleling des erreurs)
- Implémenter les references et l'ownership
- Permettre d'avoir des constantes génériques au sein d'une classe
- "Sous-typage" des interfaces (passer une classe implémentant une interface en paramètre)
Nous avons fait le choix de maintenir un code propre, flexible et facilement extensible. Toutefois, cela s'est avéré complexe, surtout à mesure que le code grossissait, ce qui a parfois ralenti notre productivité. Nous avons pris soin de gérer les erreurs de manière propre et structurée, c'est pourquoi le gestionnaire d'erreurs occupe une place importante dans le fichier kawai.ml.
Les messages d'erreur sont personnalisés, en particulier avec l'indication précise de la position où l'erreur se produit, ce qui facilite grandement le débogage. Dans l'interpréteur, le nombre d'erreurs est limité, car la majorité des erreurs sont détectées en amont par le vérificateur de types.
Nous avons implémenté la gestion des portées et du shadowing à l'aide d'une structure de données personnalisée appelée stack_env (source). Il s'agit d'une pile de tables de hachage, mais avec un "fond" commun (le dernier élément) partagé entre chaque instance de stack_env. Ce fond représente la portée globale du programme, c'est-à-dire les données accessibles à l'échelle mondiale. Chaque élément de la pile représente ensuite une portée imbriquée.
Pour résoudre une requête, il suffit de parcourir la pile de haut en bas, en cherchant la première occurrence de la donnée dans chaque table de hachage. À la fin d'une portée, le premier élément est retiré de la pile, permettant ainsi de revenir à l'environnement de portée précédent.
Le fond doit être commun à chaque instance de stack_env afin que les données globales restent accessibles, même dans des contextes totalement différents du programme principal, comme au sein des classes et méthodes.
J'ai beaucoup apprécié travailler sur ce projet, créer un langage est une expérience à vivre au moins une fois dans sa vie. Cependant, je regrette que le parser (Menhir) soit assez simple. J'y ai consacré un certain temps, notamment sur la syntaxe du typecast. Je suis conscient qu'on pourrait probablement y parvenir en ajustant les priorités des règles, mais un parser LR(1) n'est pas suffisant pour gérer, par exemple, les génériques (d'où l'introduction d'un mot-clé "generic" qui n'est pas très élégant). C'est un peu démotivant de ne pas pouvoir faire ce que l'on veut. On aurait pu utiliser la technique du "lexer hack", mais cela aurait impliqué de mélanger le lexer et le parser, ce que je n'avais pas particulièrement envie de faire 🚶.
J'aurais vraiment aimé implémenter l'ownership, cela aurait été très intéressant d'ajouter cette couche de vérification. À mon avis, cela ne semble pas trop compliqué à mettre en œuvre avec notre architecture : transmettre l'ownership signifie simplement déplacer la variable dans un autre environnement de stack (en la supprimant de l'environnement d'où elle provient). Ce n'est rien de plus ! Ce serait encore plus amusant si l'on avait implémenté les références pour gérer les lifetimes (la notion la plus complexe de Rust :p).
J'aurais aussi aimé explorer le type-level programming. J'avais l'idée de coder un algorithme de tri basé sur les types, mais pour cela, il aurait fallu des interfaces génériques et des types associés, ce qui est un peu complexe.
Nous avons finalement préféré suivre les extensions recommandées, ce qui a permis de rendre le projet plus abouti, et je pense que c'était une bonne décision !
Le projet a été pour moi l'occasion de mieux cerner les questions que se poseraient les concepteurs de langages et les raisons qui motiveraient leurs choix, et a clairement été l'un des plus formateurs de mon cursus. Comme l'a souligné mon partenaire, Menhir, bien que très pratique pour apprendre les ficelles de l'analyse de code, a été assez frustrant à utiliser par moments. L'API monolithique utilisée pour le projet nous empêche de rattraper plus d'erreurs de la forme KawaParser._error_, par exemple. La base de code existant étant assez maigre, chaque utilisation d'un outil proposé par Menhir à souvent nécéssiter pas mal de "trial and error" avant de les faire fonctionner.
Seul "regret" pour ma part est de ne pas avoir ajouté la possibilité de compiler le langage, par peur de la charge de travail que cela aurait ajouté en parallèle de celle déjà requise pour l'interpréteur.
Voici des visualisation d'ast généré avec visuast.ml
arith.kwa, multivar.kwa et shadowing.kwa
Quelques messages d'erreurs du Typechecker








