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 :
Le code source complet est disponible sur GitHub .
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 TokenType pour définir tous les types de lexème :
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 class_instance
est une instance de la classe à laquelle nous nous référons pour obtenir une classe imbriquée. La bonne expression NestedClass [args]
est une classe imbriquée avec les propriétés pour créer une instance.
Enfin, en tant qu'opérateur pour créer une classe imbriquée, j'utiliserai l'expression suivante : :: new
, 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 new
opérateur.
Avec l'ensemble actuel de lexèmes, nous n'avons qu'à ajouter une expression régulière pour l'opérateur :: new
. Cet opérateur peut être validé par l'expression regex suivante :
:{2}\\s+new
Ajoutons cette expression dans le lexème de l' Operator
en tant qu'expression OR avant la partie :{2}
représentant l'accès à la propriété de classe :
public enum TokenType { ... Operator("(\\+|-|\\*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}\\s+new|:{2}|\\(|\\)|(new|and|or)(?=\\s|$))"), ... }
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.
Pour évaluer les expressions mathématiques, nous utilisons l'algorithme Two-Stack de Dijkstra . 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'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 NestedClassInstanceOperator
en étendant
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 evaluate()
qui effectuera l'instanciation des classes imbriquées :
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 evaluate()
le bon opérande. Dans ce cas, nous ne pouvons pas invoquer directement Expression#evaluate()
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).
Pour accéder aux définitions des classes imbriquées, nous devons créer une méthode auxiliaire ClassExpression#evaluate(ClassValue)
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 :
@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 ClassExpression#evaluate()
à la seule différence que nous devons définir la ClassDefinition#getDefinitionScope()
afin de récupérer les définitions de classes imbriquées :
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(); } } }
Tous les opérateurs que nous utilisons pour évaluer les expressions mathématiques sont stockés dans l'énumération Operator avec la priorité, le caractère et le type OperatorExpression
correspondants auxquels nous nous référons pour calculer le résultat de chaque opération :
... 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 ClassInstance
pour l'initialisation d'une classe normale. Ajoutons une nouvelle valeur pour gérer les instances de classe imbriquées.
La nouvelle valeur NestedClassInstance
aura la même expression de caractère que celle que nous avons définie précédemment dans le TokenType et la même priorité que l'instance de la classe normale.
Pour le type OperatorExpression, nous utiliserons le NestedClassInstanceOperator
précédemment défini :
... 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 NestedClassInstance
à l'aide d'une expression regex, nous devons mettre à jour la méthode Operator#getType()
pour faire correspondre un opérateur avec une expression régulière :
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 NestedClassInstance
, nous devons l'injecter dans la classe ExpressionReader 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 :
if (!operators.isEmpty() && operators.peek() == Operator.ClassInstance) { operand = readClassInstance(token); }
Pour prendre en charge la lecture de l'opérateur NestedClassInstance
, nous ajoutons la condition correspondante pour l'opérateur actuel dans la pile de l'opérateur :
if (!operators.isEmpty() && (operators.peek() == Operator.ClassInstance || operators.peek() == Operator.NestedClassInstance)) { operand = readClassInstance(token); }
La méthode readClassInstance()
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 ClassExpression
sous la forme d'une expression d'opérande entière.
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.