Dans cette partie de la création de votre propre langage de programmation, nous continuerons à améliorer notre langage en implémentant des classes imbriquées et en améliorant légèrement les classes introduites dans la partie précédente. Veuillez consulter les parties précédentes : Construire votre propre langage de programmation à partir de zéro Construire votre propre langage de programmation à partir de zéro : Partie II - Algorithme à deux piles de Dijkstra Construisez votre propre langage de programmation Partie 3 : Améliorer l'analyse lexicale avec les anticipations de regex Construire votre propre langage de programmation à partir de zéro Partie IV : Implémentation de fonctions Construire votre propre langage de programmation à partir de zéro : Partie V - Tableaux Construire votre propre langage de programmation à partir de zéro : Partie VI - Boucles Construire votre propre langage de programmation à partir de zéro : Partie VII - Classes Le code source complet est disponible . sur GitHub 1 Analyse lexicale Dans la première section, nous aborderons l'analyse lexicale. En bref, c'est un processus pour diviser le code source en lexèmes de langage, tels que mot-clé, variable, opérateur, etc. Vous vous souvenez peut-être des parties précédentes que j'utilisais les expressions regex répertoriées dans l'énumération pour définir tous les types de lexème : TokenType package org.example.toylanguage.token; public enum TokenType { Comment("\\#.*"), LineBreak("[\\n\\r]"), Whitespace("[\\s\\t]"), Keyword("(if|elif|else|end|print|input|class|fun|return|loop|in|by|break|next)(?=\\s|$)"), GroupDivider("(\\[|\\]|\\,|\\{|}|[.]{2})"), Logical("(true|false)(?=\\s|$)"), Numeric("([-]?(?=[.]?[0-9])[0-9]*(?![.]{2})[.]?[0-9]*)"), Null("(null)(?=,|\\s|$)"), This("(this)(?=,|\\s|$)"), Text("\"([^\"]*)\""), Operator("(\\+|-|\\*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}|\\(|\\)|(new|and|or)(?=\\s|$))"), Variable("[a-zA-Z_]+[a-zA-Z0-9_]*"); private final String regex; } Examinons ce prototype de classes imbriquées et ajoutons les expressions regex manquantes dans nos lexèmes : TokenType Lorsque nous créons une instance d'une classe imbriquée, nous utilisons la construction suivante avec deux expressions et un opérateur entre elles : L'expression de gauche est une instance de la classe à laquelle nous nous référons pour obtenir une classe imbriquée. La bonne expression est une classe imbriquée avec les propriétés pour créer une instance. class_instance NestedClass [args] Enfin, en tant qu'opérateur pour créer une classe imbriquée, j'utiliserai l'expression suivante : , ce qui signifie que nous nous référons à la propriété d'instance de classe avec les deux deux-points opérateur, puis nous créons une instance avec le opérateur. :: new :: new Avec l'ensemble actuel de lexèmes, nous n'avons qu'à ajouter une expression régulière pour l'opérateur . Cet opérateur peut être validé par l'expression regex suivante : :: new :{2}\\s+new Ajoutons cette expression dans le lexème de l' en tant qu'expression OR avant la partie représentant l'accès à la propriété de classe : Operator :{2} public enum TokenType { ... Operator("(\\+|-|\\*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}\\s+new|:{2}|\\(|\\)|(new|and|or)(?=\\s|$))"), ... } 2 Analyse syntaxique Dans la deuxième section, nous convertirons les lexèmes reçus de l'analyseur lexical en énoncés finaux en suivant nos règles linguistiques. 2.1 OperatorExpression Pour évaluer les expressions mathématiques, nous utilisons . Chaque opération de cet algorithme peut être présentée par un opérateur unaire à un opérande ou par un opérateur binaire à respectivement deux opérandes : l'algorithme Two-Stack de Dijkstra L'instanciation de la classe imbriquée est une opération binaire où l'opérande de gauche est une instance de classe que nous utilisons pour faire référence à la classe où la classe imbriquée est définie, et le deuxième opérande est une classe imbriquée dont nous créons une instance : Créons l'implémentation en étendant NestedClassInstanceOperator : BinaryOperatorExpression package org.example.toylanguage.expression.operator; public class NestedClassInstanceOperator extends BinaryOperatorExpression { public NestedClassInstanceOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { ... } } Ensuite, nous devons compléter la méthode qui effectuera l'instanciation des classes imbriquées : evaluate() Tout d'abord, nous évaluons l'expression de l'opérande gauche dans l'expression de : Value @Override public Value<?> evaluate() { // ClassExpression -> ClassValue Value<?> left = getLeft().evaluate(); } Ensuite, nous devons le bon opérande. Dans ce cas, nous ne pouvons pas invoquer directement car les définitions des classes imbriquées sont déclarées dans le DefinitionScope de la classe parent (dans l'opérande de gauche). evaluate() Expression#evaluate() Pour accéder aux définitions des classes imbriquées, nous devons créer une méthode auxiliaire qui prendra l'opérande de gauche et utilisera son DefinitionScope pour accéder à la définition de la classe imbriquée et créer une instance de : ClassExpression#evaluate(ClassValue) @Override public Value<?> evaluate() { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue && getRight() instanceof ClassExpression) { // instantiate nested class // new Class [] :: new NestedClass [] return ((ClassExpression) getRight()).evaluate((ClassValue) left); } else { throw new ExecutionException(String.format("Unable to access class's nested class `%s``", getRight())); } } Enfin, implémentons la . ClassExpression#evaluate(ClassValue) Cette implémentation sera similaire à la à la seule différence que nous devons définir la afin de récupérer les définitions de classes imbriquées : ClassExpression#evaluate() ClassDefinition#getDefinitionScope() package org.example.toylanguage.expression; … public class ClassExpression implements Expression { private final String name; private final List<Expression> argumentExpressions; @Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList()); return evaluate(values); } /** * Evaluate nested class * * @param classValue instance of the parent class */ public Value<?> evaluate(ClassValue classValue) { //initialize class arguments List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList()); //set parent class's definition ClassDefinition classDefinition = classValue.getValue(); DefinitionContext.pushScope(classDefinition.getDefinitionScope()); try { return evaluate(values); } finally { DefinitionContext.endScope(); } } private Value<?> evaluate(List<Value<?>> values) { //get class's definition and statement ClassDefinition definition = DefinitionContext.getScope().getClass(name); ClassStatement classStatement = definition.getStatement(); //set separate scope MemoryScope classScope = new MemoryScope(null); MemoryContext.pushScope(classScope); try { //initialize constructor arguments ClassValue classValue = new ClassValue(definition, classScope); ClassInstanceContext.pushValue(classValue); IntStream.range(0, definition.getArguments().size()).boxed() .forEach(i -> MemoryContext.getScope() .setLocal(definition.getArguments().get(i), values.size() > i ? values.get(i) : NullValue.NULL_INSTANCE)); //execute function body DefinitionContext.pushScope(definition.getDefinitionScope()); try { classStatement.execute(); } finally { DefinitionContext.endScope(); } return classValue; } finally { MemoryContext.endScope(); ClassInstanceContext.popValue(); } } } 2.2 Opérateur Tous les opérateurs que nous utilisons pour évaluer les expressions mathématiques sont stockés dans l'énumération avec la priorité, le caractère et le type correspondants auxquels nous nous référons pour calculer le résultat de chaque opération : Operator OperatorExpression ... public enum Operator { Not("!", NotOperator.class, 7), ClassInstance("new", ClassInstanceOperator.class, 7), ... private final String character; private final Class<? extends OperatorExpression> type; private final Integer precedence; Operator(String character, Class<? extends OperatorExpression> type, Integer precedence) { this.character = character; this.type = type; this.precedence = precedence; } public static Operator getType(String character) { return Arrays.stream(values()) .filter(t -> Objects.equals(t.getCharacter(), character)) .findAny().orElse(null); } ... } Nous avons déjà la valeur pour l'initialisation d'une classe normale. Ajoutons une nouvelle valeur pour gérer les instances de classe imbriquées. ClassInstance La nouvelle valeur aura la même expression de caractère que celle que nous avons définie précédemment dans le et la même priorité que l'instance de la classe normale. NestedClassInstance TokenType Pour le type OperatorExpression, nous utiliserons le précédemment défini : NestedClassInstanceOperator ... public enum Operator { Not("!", NotOperator.class, 7), ClassInstance("new", ClassInstanceOperator.class, 7), NestedClassInstance(":{2}\\s+new", NestedClassInstanceOperator.class, 7), ... } Vous remarquerez peut-être que nous n'avons pas d'expressions régulières dans la propriété du caractère, à l'exception de ce nouvel opérateur. Pour lire l'opérateur à l'aide d'une expression regex, nous devons mettre à jour la méthode pour faire correspondre un opérateur avec une expression régulière : NestedClassInstance Operator#getType() public enum Operator { ... public static Operator getType(String character) { return Arrays.stream(values()) .filter(t -> character.matches(t.getCharacter())) .findAny().orElse(null); } ... } Enfin, nous devons ajouter deux barres obliques inverses avant un caractère pour les opérations contenant les symboles suivants : pour nous assurer que ces caractères ne sont pas traités comme des symboles de recherche regex : \\ +, *, (, ) Multiplication("\\*", MultiplicationOperator.class, 6), Addition("\\+", AdditionOperator.class, 5), LeftParen("\\(", 3), RightParen("\\)", 3), Après avoir introduit l'opérateur , nous devons l'injecter dans la classe qui analyse réellement les expressions mathématiques en opérandes et opérateurs. Nous avons juste besoin de trouver la ligne où nous lisons l'instance de classe : NestedClassInstance ExpressionReader if (!operators.isEmpty() && operators.peek() == Operator.ClassInstance) { operand = readClassInstance(token); } Pour prendre en charge la lecture de l'opérateur , nous ajoutons la condition correspondante pour l'opérateur actuel dans la pile de l'opérateur : NestedClassInstance if (!operators.isEmpty() && (operators.peek() == Operator.ClassInstance || operators.peek() == Operator.NestedClassInstance)) { operand = readClassInstance(token); } La méthode lira la déclaration d'une classe imbriquée avec des propriétés de la même manière qu'elle lit une déclaration de classe normale. Cette méthode renvoie l'instance de sous la forme d'une expression d'opérande entière. readClassInstance() ClassExpression 3. Conclusion Tout est prêt maintenant. Dans cette partie, nous avons implémenté des classes imbriquées comme une étape de plus vers la création d'un langage de programmation complet.