paint-brush
Construyendo su propio lenguaje de programación desde cero: Parte VIII - Clases anidadaspor@alexandermakeev
676 lecturas
676 lecturas

Construyendo su propio lenguaje de programación desde cero: Parte VIII - Clases anidadas

por Alexander Makeev9m2023/01/16
Read on Terminal Reader

Demasiado Largo; Para Leer

En esta parte de crear tu propio lenguaje de programación implementaremos clases anidadas
featured image - Construyendo su propio lenguaje de programación desde cero: Parte VIII - Clases anidadas
Alexander Makeev HackerNoon profile picture

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:


  1. Construyendo su propio lenguaje de programación desde cero
  2. Construyendo su propio lenguaje de programación desde cero: Parte II - Algoritmo de dos pilas de Dijkstra
  3. Cree su propio lenguaje de programación Parte III: Mejora del análisis léxico con expresiones regulares anticipadas
  4. Construyendo su propio lenguaje de programación desde cero Parte IV: Implementando funciones
  5. Construyendo su propio lenguaje de programación desde cero: Parte V - Matrices
  6. Construyendo su propio lenguaje de programación desde cero: Parte VI - Bucles
  7. 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 TokenType para definir todos los tipos de lexema:


 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 class_instance es una instancia de la clase a la que nos referimos para obtener una clase anidada. La expresión correcta NestedClass [args] es una clase anidada con las propiedades para crear una instancia.


Por último, como operador para crear una clase anidada, usaré la siguiente expresión: :: new , 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 new operador.


Con el conjunto actual de lexemas, solo necesitamos agregar una expresión regular para el operador :: new . Este operador se puede validar mediante la siguiente expresión regular:

 :{2}\\s+new


Agreguemos esta expresión en el lexema del Operator como expresión OR antes de la parte :{2} que representa el acceso a la propiedad de clase:

 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 el algoritmo Two-Stack de Dijkstra . 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:



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 NestedClassInstanceOperator extendiendo

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 evaluate() que realizará la creación de instancias de clases anidadas:

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 evaluate() el operando correcto. En este caso, no podemos invocar directamente Expression#evaluate() porque las definiciones de las clases anidadas se declaran en el DefinitionScope de la clase principal (en el operando izquierdo).


Para acceder a las definiciones de las clases anidadas, debemos crear un método auxiliar ClassExpression#evaluate(ClassValue) que tomará el operando izquierdo y usará su DefinitionScope para acceder a la definición de la clase anidada y crear una instancia 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())); } }


Por último, implementemos el ClassExpression#evaluate(ClassValue) que falta.


Esta implementación será similar al ClassExpression#evaluate() con la única diferencia de que debemos configurar ClassDefinition#getDefinitionScope() para recuperar definiciones de clases anidadas:

 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 operadores con la precedencia, el carácter y el tipo de expresión de OperatorExpression correspondientes a los que nos referimos para calcular el resultado de cada operación:

 ... 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 ClassInstance para la inicialización de una clase regular. Agreguemos un nuevo valor para administrar instancias de clases anidadas.


El nuevo valor NestedClassInstance tendrá la misma expresión de carácter que definimos anteriormente en el TokenType y la misma precedencia que la instancia de la clase normal.


Para el tipo OperatorExpression, usaremos el NestedClassInstanceOperator previamente definido:

 ... 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 NestedClassInstance usando una expresión regular, debemos actualizar el método Operator#getType() para hacer coincidir un operador con una expresión regular:

 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 NestedClassInstance , debemos inyectarlo en la clase ExpressionReader que realmente analiza las expresiones matemáticas en operandos y operadores. Solo necesitamos encontrar la línea donde leemos la instancia de la clase:

 if (!operators.isEmpty() && operators.peek() == Operator.ClassInstance) { operand = readClassInstance(token); }


Para admitir la lectura del operador NestedClassInstance , agregamos la condición correspondiente para el operador actual en la pila del operador:

 if (!operators.isEmpty() && (operators.peek() == Operator.ClassInstance || operators.peek() == Operator.NestedClassInstance)) { operand = readClassInstance(token); }


El método readClassInstance() 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 ClassExpression como una expresión de operando completa.

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.