En esta parte de crear su propio lenguaje de programación, continuaremos mejorando nuestro lenguaje implementando clases anidadas y actualizando ligeramente las clases presentadas en la parte anterior. Por favor, echa un vistazo a las partes anteriores: Construyendo su propio lenguaje de programación desde cero Construyendo su propio lenguaje de programación desde cero: Parte II - Algoritmo de dos pilas de Dijkstra Cree su propio lenguaje de programación Parte III: Mejora del análisis léxico con expresiones regulares anticipadas Construyendo su propio lenguaje de programación desde cero Parte IV: Implementando funciones Construyendo su propio lenguaje de programación desde cero: Parte V - Matrices Construyendo su propio lenguaje de programación desde cero: Parte VI - Bucles Construyendo su propio lenguaje de programación desde cero: Parte VII - Clases El código fuente completo está disponible . en GitHub 1 Análisis léxico 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 enumeradas en la enumeración para definir todos los tipos de lexema: 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; } Veamos este prototipo de clases anidadas y agreguemos las expresiones regulares que faltan en nuestros lexemas : TokenType Cuando creamos una instancia de una clase anidada, usamos la siguiente construcción con dos expresiones y un operador entre ellas: La expresión de la izquierda es una instancia de la clase a la que nos referimos para obtener una clase anidada. La expresión correcta es una clase anidada con las propiedades para crear una instancia. class_instance NestedClass [args] Por último, como operador para crear una clase anidada, usaré la siguiente expresión: , lo que significa que nos referimos a la propiedad de la instancia de clase con los dos dos puntos operador, y luego creamos una instancia con el operador. :: new :: new Con el conjunto actual de lexemas, solo necesitamos agregar una expresión regular para el operador . Este operador se puede validar mediante la siguiente expresión regular: :: new :{2}\\s+new Agreguemos esta expresión en el lexema del como expresión OR antes de la parte que representa el acceso a la propiedad de clase: Operator :{2} public enum TokenType { ... Operator("(\\+|-|\\*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}\\s+new|:{2}|\\(|\\)|(new|and|or)(?=\\s|$))"), ... } 2 Análisis de sintaxis En la segunda sección, convertiremos los lexemas recibidos del analizador léxico en las declaraciones finales siguiendo las reglas de nuestro idioma. 2.1 OperadorExpresión Para evaluar expresiones matemáticas, usamos . Cada operación en este algoritmo puede ser presentada por un operador unario con un operando o por un operador binario con respectivamente dos operandos: el algoritmo Two-Stack de Dijkstra La creación de instancias de clases anidadas es una operación binaria donde el operando izquierdo es una instancia de clase que usamos para referirnos a la clase donde se define la clase anidada, y el segundo operando es una clase anidada de la que creamos una instancia: Vamos a crear la implementación de extendiendo NestedClassInstanceOperator : ExpresiónOperadorBinario package org.example.toylanguage.expression.operator; public class NestedClassInstanceOperator extends BinaryOperatorExpression { public NestedClassInstanceOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { ... } } A continuación, debemos completar el método de que realizará la creación de instancias de clases anidadas: evaluate() Primero, evaluamos la expresión del operando izquierdo en la expresión : Value @Override public Value<?> evaluate() { // ClassExpression -> ClassValue Value<?> left = getLeft().evaluate(); } A continuación, necesitamos el operando correcto. En este caso, no podemos invocar directamente porque las definiciones de las clases anidadas se declaran en el DefinitionScope de la clase principal (en el operando izquierdo). evaluate() Expression#evaluate() Para acceder a las definiciones de las clases anidadas, debemos crear un método auxiliar que tomará el operando izquierdo y usará su DefinitionScope para acceder a la definición de la clase anidada y crear una instancia 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())); } } Por último, implementemos el que falta. ClassExpression#evaluate(ClassValue) Esta implementación será similar al con la única diferencia de que debemos configurar para recuperar definiciones de clases anidadas: 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 Operador Todos los operadores que usamos para evaluar expresiones matemáticas se almacenan en la enumeración de con la precedencia, el carácter y el tipo de expresión de correspondientes a los que nos referimos para calcular el resultado de cada operación: operadores 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); } ... } Ya tenemos el valor para la inicialización de una clase regular. Agreguemos un nuevo valor para administrar instancias de clases anidadas. ClassInstance El nuevo valor tendrá la misma expresión de carácter que definimos anteriormente en el y la misma precedencia que la instancia de la clase normal. NestedClassInstance TokenType Para el tipo OperatorExpression, usaremos el previamente definido: NestedClassInstanceOperator ... public enum Operator { Not("!", NotOperator.class, 7), ClassInstance("new", ClassInstanceOperator.class, 7), NestedClassInstance(":{2}\\s+new", NestedClassInstanceOperator.class, 7), ... } Puede notar que no tenemos expresiones regulares en la propiedad del carácter, excepto por este nuevo operador. Para leer el operador usando una expresión regular, debemos actualizar el método para hacer coincidir un operador con una expresión regular: 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); } ... } Por último, debemos agregar dos barras invertidas antes de un carácter para operaciones que contengan los siguientes símbolos: para asegurarnos de que estos caracteres no se traten como símbolos de búsqueda de expresiones regulares: \\ +, *, (, ) Multiplication("\\*", MultiplicationOperator.class, 6), Addition("\\+", AdditionOperator.class, 5), LeftParen("\\(", 3), RightParen("\\)", 3), Después de que introdujimos el operador , debemos inyectarlo en la clase que realmente analiza las expresiones matemáticas en operandos y operadores. Solo necesitamos encontrar la línea donde leemos la instancia de la clase: NestedClassInstance ExpressionReader if (!operators.isEmpty() && operators.peek() == Operator.ClassInstance) { operand = readClassInstance(token); } Para admitir la lectura del operador , agregamos la condición correspondiente para el operador actual en la pila del operador: NestedClassInstance if (!operators.isEmpty() && (operators.peek() == Operator.ClassInstance || operators.peek() == Operator.NestedClassInstance)) { operand = readClassInstance(token); } El método leerá la declaración de una clase anidada con propiedades de la misma manera que lee una declaración de clase normal. Este método devuelve la instancia de como una expresión de operando completa. readClassInstance() ClassExpression 3. Cierre Todo está listo ahora. En esta parte, implementamos clases anidadas como un paso más para hacer un lenguaje de programación completo.