paint-brush
Construindo sua própria linguagem de programação do zero: Parte VIII - Classes aninhadaspor@alexandermakeev
676 leituras
676 leituras

Construindo sua própria linguagem de programação do zero: Parte VIII - Classes aninhadas

por Alexander Makeev9m2023/01/16
Read on Terminal Reader

Muito longo; Para ler

Nesta parte da criação de sua própria linguagem de programação, implementaremos classes aninhadas
featured image - Construindo sua própria linguagem de programação do zero: Parte VIII - Classes aninhadas
Alexander Makeev HackerNoon profile picture

Nesta parte da criação de sua própria linguagem de programação, continuaremos melhorando nossa linguagem implementando classes aninhadas e atualizando ligeiramente as classes introduzidas na parte anterior. Confira as partes anteriores:


  1. Construindo sua própria linguagem de programação do zero
  2. Construindo sua própria linguagem de programação do zero: Parte II - Algoritmo de duas pilhas de Dijkstra
  3. Crie sua própria linguagem de programação Parte III: Melhorando a análise léxica com Regex Lookaheads
  4. Construindo sua própria linguagem de programação do zero Parte IV: Implementando funções
  5. Construindo sua própria linguagem de programação do zero: Parte V - Arrays
  6. Construindo sua própria linguagem de programação do zero: Parte VI - Loops
  7. Construindo sua própria linguagem de programação do zero: Parte VII - Aulas


O código-fonte completo está disponível no GitHub .

1 Análise Lexical

Na primeira seção, abordaremos a análise lexical. Resumindo, é um processo para dividir o código-fonte em lexemas de linguagem, como palavra-chave, variável, operador, etc.


Você deve se lembrar das partes anteriores que eu estava usando as expressões regex listadas na enumeração TokenType para definir todos os tipos de lexemas:


 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; }


Vamos examinar este protótipo de classes aninhadas e adicionar as expressões regex ausentes em nossos lexemas TokenType :


Quando criamos uma instância de uma classe aninhada, usamos a seguinte construção com duas expressões e um operador entre elas:

A expressão à esquerda class_instance é uma instância da classe da qual estamos nos referindo para obter uma classe aninhada. A expressão correta NestedClass [args] é uma classe aninhada com as propriedades para criar uma instância.


Por fim, como um operador para criar uma classe aninhada, usarei a seguinte expressão: :: new , o que significa que nos referimos à propriedade da instância da classe com dois pontos :: operador e, em seguida, criamos uma instância com o new operador.


Com o conjunto atual de lexemas, precisamos apenas adicionar uma expressão regular para o operador :: new . Este operador pode ser validado pela seguinte expressão regex:

 :{2}\\s+new


Vamos adicionar esta expressão no lexema do Operator como expressão OR antes da parte :{2} que significa acessar a propriedade da classe:

 public enum TokenType { ... Operator("(\\+|-|\\*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}\\s+new|:{2}|\\(|\\)|(new|and|or)(?=\\s|$))"), ... }


2 Análise de Sintaxe

Na segunda seção, converteremos os lexemas recebidos do analisador léxico nos enunciados finais seguindo nossas regras de linguagem.

2.1 Expressão do Operador

Para avaliar expressões matemáticas, estamos usando o algoritmo Two-Stack de Dijkstra . Cada operação neste algoritmo pode ser apresentada por um operador unário com um operando ou por um operador binário com dois operandos respectivamente:



A instanciação da classe aninhada é uma operação binária em que o operando esquerdo é uma instância de classe que usamos para nos referir à classe em que a classe aninhada é definida e o segundo operando é uma classe aninhada da qual criamos uma instância:


Vamos criar a implementação NestedClassInstanceOperator estendendo

BinaryOperatorExpression :

 package org.example.toylanguage.expression.operator; public class NestedClassInstanceOperator extends BinaryOperatorExpression { public NestedClassInstanceOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { ... } }


Em seguida, devemos concluir o método evaluate() que realizará a instanciação de classes aninhadas:

Primeiro, avaliamos a expressão do operando esquerdo na expressão Value :

 @Override public Value<?> evaluate() { // ClassExpression -> ClassValue Value<?> left = getLeft().evaluate(); }


Em seguida, precisamos evaluate() o operando correto. Nesse caso, não podemos chamar Expression#evaluate() diretamente porque as definições das classes aninhadas são declaradas no DefinitionScope da classe pai (no operando esquerdo).


Para acessar as definições de classes aninhadas, devemos criar um método auxiliar ClassExpression#evaluate(ClassValue) que pegará o operando esquerdo e usará seu DefinitionScope para acessar a definição de classe aninhada e criar uma instância 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 fim, vamos implementar o ClassExpression#evaluate(ClassValue) .


Esta implementação será semelhante ao ClassExpression#evaluate() com a única diferença sendo que devemos definir o ClassDefinition#getDefinitionScope() para recuperar definições de classes aninhadas:

 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 os operadores que estamos usando para avaliar expressões matemáticas são armazenados na enumeração Operator com a precedência, caractere e tipo OperatorExpression correspondentes aos quais nos referimos para calcular o resultado de cada operação:

 ... 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); } ... }


Já temos o valor ClassInstance para a inicialização de uma classe regular. Vamos adicionar um novo valor para gerenciar instâncias de classes aninhadas.


O novo valor NestedClassInstance terá a mesma expressão de caractere que definimos no TokenType anteriormente e a mesma precedência da instância da classe regular.


Para o tipo OperatorExpression, usaremos o NestedClassInstanceOperator definido anteriormente:

 ... public enum Operator { Not("!", NotOperator.class, 7), ClassInstance("new", ClassInstanceOperator.class, 7), NestedClassInstance(":{2}\\s+new", NestedClassInstanceOperator.class, 7), ... }


Você pode perceber que não temos expressões regex na propriedade de caractere, exceto para este novo operador. Para ler o operador NestedClassInstance usando uma expressão regex, devemos atualizar o método Operator#getType() para corresponder a um operador com uma expressão regular:

 public enum Operator { ... public static Operator getType(String character) { return Arrays.stream(values()) .filter(t -> character.matches(t.getCharacter())) .findAny().orElse(null); } ... }


Por fim, devemos adicionar duas barras invertidas \\ antes de um caractere para operações que contenham os seguintes símbolos: +, *, (, ) para garantir que esses caracteres não sejam tratados como símbolos de pesquisa regex:

 Multiplication("\\*", MultiplicationOperator.class, 6), Addition("\\+", AdditionOperator.class, 5), LeftParen("\\(", 3), RightParen("\\)", 3),


Depois de introduzirmos o operador NestedClassInstance , devemos injetá-lo na classe ExpressionReader que realmente analisa expressões matemáticas em operandos e operadores. Só precisamos encontrar a linha onde lemos a instância da classe:

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


Para dar suporte à leitura do operador NestedClassInstance , adicionamos a condição correspondente para o operador atual na pilha do operador:

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


O método readClassInstance() lerá a declaração de uma classe aninhada com propriedades da mesma forma que lê uma declaração de classe regular. Este método retorna a instância ClassExpression como uma expressão de operando inteira.

3. Conclusão

Tudo está pronto agora. Nesta parte, implementamos classes aninhadas como mais uma etapa para criar uma linguagem de programação completa.