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:
O código-fonte completo está disponível no GitHub .
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|$))"), ... }
Na segunda seção, converteremos os lexemas recebidos do analisador léxico nos enunciados finais seguindo nossas regras de linguagem.
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
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(); } } }
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.
Tudo está pronto agora. Nesta parte, implementamos classes aninhadas como mais uma etapa para criar uma linguagem de programação completa.