En esta parte de crear tu propio lenguaje de programación, implementaremos clases como una extensión sobre las estructuras previamente definidas. Por favor, echa un vistazo a las partes anteriores:
El código fuente completo está disponible.
En la primera sección, cubriremos el análisis léxico. En resumen, es un proceso para dividir el código fuente en lexemas de lenguaje, como palabra clave, variable, operador, etc.
Puede recordar de las partes anteriores que estaba usando las expresiones regulares en la enumeración TokenType para definir todos los tipos de lexema.
Veamos el siguiente prototipo de clase y agreguemos las partes de expresiones regulares que faltan a nuestros lexemas TokenType
:
class
en la expresión del lexema de Keyword
para que el analizador léxico sepa dónde comienza nuestra declaración de clase: 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
como marcador para una referencia al objeto actual: public enum TokenType { ... This("(this)(?=,|\\s|$)"); private final String regex; }
En la segunda sección, transformaremos los lexemas recibidos del analizador léxico en las declaraciones finales siguiendo nuestras reglas de lenguaje.
Cuando declaramos una clase o una función, esta declaración debe estar disponible dentro de los límites aislados definidos. Por ejemplo, si declaramos una función llamada turn_on []
en el siguiente listado, estará disponible para su ejecución después de la declaración:
Pero si declaramos la misma función dentro de un ámbito de clase, ya no se podrá acceder a esta función directamente desde el bloque principal:
DefinitionScope
y almacenaremos todas las definiciones declaradas dentro de dos conjuntos para clases y funciones: 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<>(); } }
Para proporcionar esta capacidad, agregaremos la instancia principal de DefinitionScope
como referencia a la capa superior, que usaremos para subir a la capa de definición superior.
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(); } }
En esta sección, cubriremos el MemoryScope
para administrar variables de clase y función.
Pero si declaramos una variable dentro de una función o una clase, la variable ya no estará disponible desde el bloque de código principal (superior):
Para implementar esta lógica y almacenar variables definidas en un ámbito específico, creamos la clase MemoryScope
que contendrá un mapa con el nombre de la variable como clave y la variable Value
como valor:
public class MemoryScope { private final Map<String, Value<?>> variables; public MemoryScope() { this.variables = new HashMap<>(); } }
DefinitionScope
, brindamos acceso a las variables de alcance de los padres: 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
y get
, agregamos dos implementaciones más para interactuar con la capa actual (local) 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); } }
Estos métodos se utilizarán más adelante para inicializar argumentos de funciones o argumentos de instancias de clases. Por ejemplo, si creamos una instancia de la clase Lamp
y pasamos la variable de type
global predefinida, esta variable no debería cambiarse cuando intentemos actualizar la propiedad lamp_instance :: type
:
MemoryContext
usando la colección 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(); } }
En esta sección, leeremos y almacenaremos definiciones de clases.
package org.example.toylanguage.statement; public class ClassStatement { }
public class ClassStatement extends CompositeStatement { }
ClassDefinition
para almacenar el nombre de la clase, sus argumentos, las declaraciones del constructor y el alcance de la definición con las funciones de la clase: 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
, primero debemos leer el nombre de la clase y sus argumentos dentro de los corchetes:
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 } } }
Para almacenar estas declaraciones, creamos una instancia del ClassStatement
previamente definido:
private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); }
DefinitionScope
: private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); }
ClassDefinition
y la colocamos en 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
hasta que encontremos el lexema end
que debe omitirse al final: private void parseClassDefinition() { ... //parse class statements StatementParser.parse(this, classStatement, classScope); tokens.next(TokenType.Keyword, "end"); }
En este punto, ya podemos leer definiciones de clase con declaraciones y funciones del constructor. Ahora, analicemos la instancia de la clase:
ClassValue
, que contendrá el estado de cada instancia de clase. Las clases, a diferencia de las funciones, deben tener un MemoryScope
permanente y este alcance debe estar disponible con todos los argumentos de la instancia de la clase y las variables de estado cada vez que interactuamos con la instancia de la clase: 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()
y MemoryScope#setLocal()
, trabajamos con la capa actual de variables de MemoryScope
. Pero antes de acceder al estado de instancia de la clase, debemos colocar su MemoryScope
en MemoryContext
y liberarlo cuando terminemos: 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 que se usará para construir instancias de clase definidas durante el análisis de sintaxis. Para declarar la definición de instancia de la clase, proporcionamos la ClassDefinition
de clase y la lista de argumentos de Expression
que se transformarán en las instancias de Value
final durante la ejecución de la declaración: 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()
que se usará durante la ejecución para crear una instancia del ClassValue
previamente definido. Primero, evaluamos los argumentos de Expression
en los argumentos de 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
y escribimos los argumentos de Value
de la clase en el ámbito de la memoria aislada: 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(); }
Tenga en cuenta que transformamos los argumentos de Expression
en argumentos de Value
antes de configurar un MemoryScope
vacío. De lo contrario, no podremos acceder a los argumentos de instancia de la clase, por ejemplo:
ClassStatement
. Pero antes de eso, debemos configurar el DefinitionScope
de la clase para poder acceder a las funciones de la clase dentro de las declaraciones del constructor: 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*. Una última cosa, podemos hacer que las clases sean más flexibles y permitir que un usuario cree instancias de clase antes de declarar la definición de clase:
Esto se puede hacer delegando la inicialización de ClassDefinition
al DefinitionContext
y accediendo a él solo cuando evaluamos una expresión:
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); ... } }
Puede hacer la misma delegación para FunctionExpression para invocar funciones antes de la definición.
ExpressionReader
. No hay diferencia entre las instancias de estructura definidas anteriormente . Solo necesitamos leer los argumentos de Expression
y construir 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); } }
En este momento, podemos crear una clase y ejecutar las declaraciones del constructor de la clase. Pero aún no podemos ejecutar las funciones de la clase.
FunctionExpression#evaluate
que aceptará ClassValue
como una referencia a la instancia de clase desde la que queremos invocar una función: package org.example.toylanguage.expression; public class FunctionExpression implements Expression { ... public Value<?> evaluate(ClassValue classValue) { } }
Expression
de la función en argumentos de Value
utilizando el MemoryScope
actual: public Value<?> evaluate(ClassValue classValue) { //initialize function arguments List<Value<?>> values = argumentExpressions.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }
MemoryScope
y DefinitionScope
de la clase al contexto: ... //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
evaluados: 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(); } }
::
. Actualmente, este operador es administrado por la ClassPropertyOperator
( StructureValueOperator ), que se encarga de acceder a las propiedades de la clase:
Mejorémoslo para admitir invocaciones de funciones con el mismo carácter de dos puntos ::
La función de clase puede ser administrada por este operador solo cuando la expresión de la izquierda es ClassExpression
y la segunda es 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); } } }
En esta parte, implementamos clases. Ahora podemos crear cosas más complejas, como la implementación de pilas:
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