Dans cette partie de la création de votre propre langage de programmation, nous allons implémenter des classes en tant qu'extension sur les structures précédemment définies. Veuillez consulter les parties précédentes :
Le code source complet est disponible
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 dans l'énumération TokenType pour définir tous les types de lexème.
Examinons le prototype de classe suivant et ajoutons les parties de regex manquantes à nos lexèmes TokenType
:
class
dans l'expression du lexème de mot- Keyword
pour que l'analyseur lexical sache où commence notre déclaration de classe : package org.example.toylanguage.token; ... public enum TokenType { ... Keyword("(if|elif|else|end|print|input|fun|return|loop|in|by|break|next|class)(?=\\s|$)"), ... private final String regex; }
This
comme marqueur d'une référence à l'objet courant : public enum TokenType { ... This("(this)(?=,|\\s|$)"); private final String regex; }
Dans la deuxième section, nous transformerons les lexèmes reçus de l'analyseur lexical en énoncés finaux selon nos règles linguistiques.
Lorsque nous déclarons une classe ou une fonction, cette déclaration doit être disponible dans les limites isolées définies. Par exemple, si nous déclarons une fonction nommée turn_on []
dans le listing suivant, elle sera disponible pour exécution après déclaration :
Mais si nous déclarons la même fonction à l'intérieur d'une portée de classe, cette fonction ne sera plus accessible directement depuis le bloc principal :
DefinitionScope
et stocker toutes les définitions déclarées dans deux ensembles pour les classes et les fonctions : package org.example.toylanguage.context.definition; public class DefinitionScope { private final Set<ClassDefinition> classes; private final Set<FunctionDefinition> functions; public DefinitionScope() { this.classes = new HashSet<>(); this.functions = new HashSet<>(); } }
Pour fournir cette capacité, nous ajouterons l'instance parent DefinitionScope
comme référence à la couche supérieure, que nous utiliserons pour monter à la couche de définition supérieure.
public class DefinitionScope { private final Set<ClassDefinition> classes; private final Set<FunctionDefinition> functions; private final DefinitionScope parent; public DefinitionScope(DefinitionScope parent) { this.classes = new HashSet<>(); this.functions = new HashSet<>(); this.parent = parent; } }
public class DefinitionScope { … public ClassDefinition getClass(String name) { Optional<ClassDefinition> classDefinition = classes.stream() .filter(t -> t.getName().equals(name)) .findAny(); if (classDefinition.isPresent()) return classDefinition.get(); else if (parent != null) return parent.getClass(name); else throw new ExecutionException(String.format("Class is not defined: %s", name)); } public void addClass(ClassDefinition classDefinition) { classes.add(classDefinition); } public FunctionDefinition getFunction(String name) { Optional<FunctionDefinition> functionDefinition = functions.stream() .filter(t -> t.getName().equals(name)) .findAny(); if (functionDefinition.isPresent()) return functionDefinition.get(); else if (parent != null) return parent.getFunction(name); else throw new ExecutionException(String.format("Function is not defined: %s", name)); } public void addFunction(FunctionDefinition functionDefinition) { functions.add(functionDefinition); } }
java.util.Stack
(LIFO) : package org.example.toylanguage.context.definition; public class DefinitionContext { private final static Stack<DefinitionScope> scopes = new Stack<>(); public static DefinitionScope getScope() { return scopes.peek(); } public static DefinitionScope newScope() { return new DefinitionScope(scopes.isEmpty() ? null : scopes.peek()); } public static void pushScope(DefinitionScope scope) { scopes.push(scope); } public static void endScope() { scopes.pop(); } }
Dans cette section, nous couvrirons le MemoryScope
pour gérer les variables de classe et de fonction.
Mais si nous déclarons une variable dans une fonction ou une classe, la variable ne sera plus disponible depuis le bloc de code principal (supérieur) :
Pour implémenter cette logique et stocker des variables définies dans une portée spécifique, nous créons la classe MemoryScope
qui contiendra une carte avec le nom de la variable comme clé et la variable Value
comme valeur :
public class MemoryScope { private final Map<String, Value<?>> variables; public MemoryScope() { this.variables = new HashMap<>(); } }
DefinitionScope
, nous donnons accès aux variables de portée du parent : public class MemoryScope { private final Map<String, Value<?>> variables; private final MemoryScope parent; public MemoryScope(MemoryScope parent) { this.variables = new HashMap<>(); this.parent = parent; } }
MemoryScope
: public class MemoryScope { ... public Value<?> get(String name) { Value<?> value = variables.get(name); if (value != null) return value; else if (parent != null) return parent.get(name); else return NullValue.NULL_INSTANCE; } public void set(String name, Value<?> value) { MemoryScope variableScope = findScope(name); if (variableScope == null) { variables.put(name, value); } else { variableScope.variables.put(name, value); } } private MemoryScope findScope(String name) { if (variables.containsKey(name)) return this; return parent == null ? null : parent.findScope(name); } }
set
et get
, nous ajoutons deux autres implémentations pour interagir avec la couche actuelle (locale) de MemoryScope
: public class MemoryScope { ... public Value<?> getLocal(String name) { return variables.get(name); } public void setLocal(String name, Value<?> value) { variables.put(name, value); } }
Ces méthodes seront utilisées plus tard pour initialiser les arguments de la fonction ou les arguments de l'instance de la classe. Par exemple, si nous créons une instance de la classe Lamp
et passons la variable de type
globale prédéfinie, cette variable ne doit pas être modifiée lorsque nous essayons de mettre à jour la propriété lamp_instance :: type
:
MemoryContext
à l'aide de la collection java.util.Stack
: package org.example.toylanguage.context; public class MemoryContext { private static final Stack<MemoryScope> scopes = new Stack<>(); public static MemoryScope getScope() { return scopes.peek(); } public static MemoryScope newScope() { return new MemoryScope(scopes.isEmpty() ? null : scopes.peek()); } public static void pushScope(MemoryScope scope) { scopes.push(scope); } public static void endScope() { scopes.pop(); } }
Dans cette section, nous allons lire et stocker les définitions de classe.
package org.example.toylanguage.statement; public class ClassStatement { }
public class ClassStatement extends CompositeStatement { }
ClassDefinition
pour stocker le nom de la classe, ses arguments, les instructions du constructeur et la portée de la définition avec les fonctions de la classe : package org.example.toylanguage.context.definition; import java.util.List; @RequiredArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class ClassDefinition implements Definition { @EqualsAndHashCode.Include private final String name; private final List<String> arguments; private final ClassStatement statement; private final DefinitionScope definitionScope; }
class
, nous devons d'abord lire le nom de la classe et ses arguments entre crochets :
package org.example.toylanguage; public class StatementParser { ... private void parseClassDefinition() { Token type = tokens.next(TokenType.Variable); List<String> arguments = new ArrayList<>(); if (tokens.peek(TokenType.GroupDivider, "[")) { tokens.next(TokenType.GroupDivider, "["); //skip opening square bracket while (!tokens.peek(TokenType.GroupDivider, "]")) { Token argumentToken = tokens.next(TokenType.Variable); arguments.add(argumentToken.getValue()); if (tokens.peek(TokenType.GroupDivider, ",")) tokens.next(); } tokens.next(TokenType.GroupDivider, "]"); //skip closing square bracket } } }
Pour stocker ces instructions, nous créons une instance du ClassStatement
précédemment défini :
private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); }
DefinitionScope
: private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); }
ClassDefinition
et la plaçons dans le DefinitionContext
: private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); ClassDefinition classDefinition = new ClassDefinition(type.getValue(), arguments, classStatement, classScope); DefinitionContext.getScope().addClass(classDefinition); }
classStatement
jusqu'à ce que nous rencontrions le lexème de end
de finalisation qui doit être ignoré à la fin : private void parseClassDefinition() { ... //parse class statements StatementParser.parse(this, classStatement, classScope); tokens.next(TokenType.Keyword, "end"); }
À ce stade, nous pouvons déjà lire les définitions de classe avec les instructions et les fonctions du constructeur. Maintenant, analysons l'instance de la classe :
ClassValue
, qui contiendra l'état de chaque instance de classe. Les classes, contrairement aux fonctions, doivent avoir une MemoryScope
permanente et cette portée doit être disponible avec tous les arguments d'instance de la classe et les variables d'état chaque fois que nous interagissons avec l'instance de classe : public class ClassValue extends IterableValue<ClassDefinition> { private final MemoryScope memoryScope; public ClassValue(ClassDefinition definition, MemoryScope memoryScope) { super(definition); this.memoryScope = memoryScope; } }
MemoryContext
: public class ClassValue extends IterableValue<ClassDefinition> { private final MemoryScope memoryScope; public ClassValue(ClassDefinition definition, MemoryScope memoryScope) { super(definition); this.memoryScope = memoryScope; } @Override public String toString() { return getValue().getArguments().stream() .map(t -> t + " = " + getValue(t)) .collect(Collectors.joining(", ", getValue().getName() + " [ ", " ]")); } public Value<?> getValue(String name) { Value<?> result = MemoryContext.getScope().getLocal(name); return result != null ? result : NULL_INSTANCE; } public void setValue(String name, Value<?> value) { MemoryContext.getScope().setLocal(name, value); } }
MemoryScope#getLocal()
et MemoryScope#setLocal()
, nous travaillons avec la couche actuelle de variables MemoryScope
. Mais avant d'accéder à l'état de l'instance de la classe, nous devons mettre son MemoryScope
dans le MemoryContext
et le libérer lorsque nous avons terminé : public class ClassValue extends IterableValue<ClassDefinition> { ... public Value<?> getValue(String name) { MemoryContext.pushScope(memoryScope); try { Value<?> result = MemoryContext.getScope().getLocal(name); return result != null ? result : NULL_INSTANCE; } finally { MemoryContext.endScope(); } } public void setValue(String name, Value<?> value) { MemoryContext.pushScope(memoryScope); try { MemoryContext.getScope().setLocal(name, value); } finally { MemoryContext.endScope(); } } }
ClassExpression
restante qui sera utilisée pour construire des instances de classe définies lors de l'analyse de la syntaxe. Pour déclarer la définition d'instance de la classe, nous fournissons la ClassDefinition
et la liste des arguments Expression
qui seront transformés en instances Value
finales lors de l'exécution de l'instruction : package org.example.toylanguage.expression; @RequiredArgsConstructor @Getter public class ClassExpression implements Expression { private final ClassDefinition definition; private final List<Expression> arguments; @Override public Value<?> evaluate() { ... } }
Expression#evaluate()
qui sera utilisée lors de l'exécution pour créer une instance de la ClassValue
précédemment définie. Tout d'abord, nous évaluons les arguments Expression
dans les arguments Value
: @Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = arguments.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }
@Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = arguments.stream().map(Expression::evaluate).collect(Collectors.toList()); //get class's definition and statement ClassStatement classStatement = definition.getStatement(); //set separate scope MemoryScope classScope = new MemoryScope(null); MemoryContext.pushScope(classScope); try { ... } finally { MemoryContext.endScope(); } }
ClassValue
et écrivons les arguments Value
de la classe dans la portée de mémoire isolée : try { //initialize constructor arguments ClassValue classValue = new ClassValue(definition, classScope); 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)); ... } finally { MemoryContext.endScope(); }
Veuillez noter que nous avons transformé les arguments Expression
en arguments Value
avant de configurer un MemoryScope
vide. Sinon, nous ne pourrons pas accéder aux arguments d'instance de la classe, par exemple :
ClassStatement
. Mais avant cela, nous devons définir le DefinitionScope
de la classe pour pouvoir accéder aux fonctions de la classe dans les instructions du constructeur : 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(); }
9*. Une dernière chose, nous pouvons rendre les classes plus flexibles et permettre à un utilisateur de créer des instances de classe avant de déclarer la définition de classe :
Cela peut être fait en déléguant l'initialisation de ClassDefinition
au DefinitionContext
et en y accédant uniquement lorsque nous évaluons une expression :
public class ClassExpression implements Expression { private final String name; private final List<Expression> arguments; @Override public Value<?> evaluate() { //get class's definition and statement ClassDefinition definition = DefinitionContext.getScope().getClass(name); ... } }
Vous pouvez faire la même délégation pour la FunctionExpression pour appeler des fonctions avant la définition.
ExpressionReader
. Il n'y a aucune différence entre les instances de structure précédemment définies . Nous avons juste besoin de lire les arguments Expression
et de construire le ClassExpression
: public class ExpressionReader ... // read class instance: new Class[arguments] private ClassExpression readClassInstance(Token token) { List<Expression> arguments = new ArrayList<>(); if (tokens.peekSameLine(TokenType.GroupDivider, "[")) { tokens.next(TokenType.GroupDivider, "["); //skip opening square bracket while (!tokens.peekSameLine(TokenType.GroupDivider, "]")) { Expression value = ExpressionReader.readExpression(this); arguments.add(value); if (tokens.peekSameLine(TokenType.GroupDivider, ",")) tokens.next(); } tokens.next(TokenType.GroupDivider, "]"); //skip closing square bracket } return new ClassExpression(token.getValue(), arguments); } }
À ce moment, nous pouvons créer une classe et exécuter les instructions du constructeur de la classe. Mais nous sommes toujours incapables d'exécuter les fonctions de la classe.
FunctionExpression#evaluate
qui acceptera la ClassValue
comme référence à l'instance de classe à partir de laquelle nous voulons invoquer une fonction : package org.example.toylanguage.expression; public class FunctionExpression implements Expression { ... public Value<?> evaluate(ClassValue classValue) { } }
Expression
de la fonction en arguments Value
à l'aide de MemoryScope
: public Value<?> evaluate(ClassValue classValue) { //initialize function arguments List<Value<?>> values = argumentExpressions.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }
MemoryScope
et DefinitionScope
de la classe au contexte : ... //get definition and memory scopes from class definition ClassDefinition classDefinition = classValue.getValue(); DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope(); //set class's definition and memory scopes DefinitionContext.pushScope(classDefinitionScope); MemoryContext.pushScope(memoryScope);
Value
évalués : public Value<?> evaluate(ClassValue classValue) { //initialize function arguments List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList()); //get definition and memory scopes from class definition ClassDefinition classDefinition = classValue.getValue(); DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope(); MemoryScope memoryScope = classValue.getMemoryScope(); //set class's definition and memory scopes DefinitionContext.pushScope(classDefinitionScope); MemoryContext.pushScope(memoryScope); try { //proceed function return evaluate(values); } finally { DefinitionContext.endScope(); MemoryContext.endScope(); } }
::
. Actuellement, cet opérateur est géré par l' ClassPropertyOperator
( StructureValueOperator ), qui est responsable de l'accès aux propriétés de la classe :
Améliorons-le pour prendre en charge les invocations de fonction avec le même caractère double deux-points ::
:
La fonction de la classe peut être gérée par cet opérateur uniquement lorsque l'expression de gauche est la ClassExpression
et la seconde est la FunctionExpression
:
package org.example.toylanguage.expression.operator; public class ClassPropertyOperator extends BinaryOperatorExpression implements AssignExpression { public ClassPropertyOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue) { if (getRight() instanceof VariableExpression) { // access class's property // new ClassInstance[] :: class_argument return ((ClassValue) left).getValue(((VariableExpression) getRight()).getName()); } else if (getRight() instanceof FunctionExpression) { // execute class's function // new ClassInstance[] :: class_function [] return ((FunctionExpression) getRight()).evaluate((ClassValue) left); } } throw new ExecutionException(String.format("Unable to access class's property `%s``", getRight())); } @Override public void assign(Value<?> value) { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue && getRight() instanceof VariableExpression) { String propertyName = ((VariableExpression) getRight()).getName(); ((ClassValue) left).setValue(propertyName, value); } } }
Dans cette partie, nous avons implémenté des classes. Nous pouvons maintenant créer des choses plus complexes, telles que l'implémentation de la pile :
main [] fun main [] stack = new Stack [] loop num in 0..5 # push 0,1,2,3,4 stack :: push [ num ] end size = stack :: size [] loop i in 0..size # should print 4,3,2,1,0 print stack :: pop [] end end class Stack [] arr = {} n = 0 fun push [ item ] this :: arr << item n = n + 1 end fun pop [] n = n - 1 item = arr { n } arr { n } = null return item end fun size [] return this :: n end end