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: Construindo sua própria linguagem de programação do zero Construindo sua própria linguagem de programação do zero: Parte II - Algoritmo de duas pilhas de Dijkstra Crie sua própria linguagem de programação Parte III: Melhorando a análise léxica com Regex Lookaheads Construindo sua própria linguagem de programação do zero Parte IV: Implementando funções Construindo sua própria linguagem de programação do zero: Parte V - Arrays Construindo sua própria linguagem de programação do zero: Parte VI - Loops 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 para definir todos os tipos de lexemas: 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; } 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 é uma instância da classe da qual estamos nos referindo para obter uma classe aninhada. A expressão correta é uma classe aninhada com as propriedades para criar uma instância. class_instance NestedClass [args] Por fim, como um operador para criar uma classe aninhada, usarei a seguinte expressão: , 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 operador. :: new :: new Com o conjunto atual de lexemas, precisamos apenas adicionar uma expressão regular para o operador . Este operador pode ser validado pela seguinte expressão regex: :: new :{2}\\s+new Vamos adicionar esta expressão no lexema do como expressão OR antes da parte que significa acessar a propriedade da classe: Operator :{2} 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 . 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: o algoritmo Two-Stack de Dijkstra 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 estendendo NestedClassInstanceOperator : 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 que realizará a instanciação de classes aninhadas: evaluate() 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 o operando correto. Nesse caso, não podemos chamar diretamente porque as definições das classes aninhadas são declaradas no DefinitionScope da classe pai (no operando esquerdo). evaluate() Expression#evaluate() Para acessar as definições de classes aninhadas, devemos criar um método auxiliar que pegará o operando esquerdo e usará seu DefinitionScope para acessar a definição de classe aninhada e criar uma instância 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 fim, vamos implementar o . ClassExpression#evaluate(ClassValue) Esta implementação será semelhante ao com a única diferença sendo que devemos definir o para recuperar definições de classes aninhadas: 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 os operadores que estamos usando para avaliar expressões matemáticas são armazenados na enumeração com a precedência, caractere e tipo correspondentes aos quais nos referimos para calcular o resultado de cada operação: Operator 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); } ... } Já temos o valor para a inicialização de uma classe regular. Vamos adicionar um novo valor para gerenciar instâncias de classes aninhadas. ClassInstance O novo valor terá a mesma expressão de caractere que definimos no anteriormente e a mesma precedência da instância da classe regular. NestedClassInstance TokenType Para o tipo OperatorExpression, usaremos o definido anteriormente: NestedClassInstanceOperator ... 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 usando uma expressão regex, devemos atualizar o método para corresponder a um operador com uma expressão 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 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 , devemos injetá-lo na classe que realmente analisa expressões matemáticas em operandos e operadores. Só precisamos encontrar a linha onde lemos a instância da classe: NestedClassInstance ExpressionReader if (!operators.isEmpty() && operators.peek() == Operator.ClassInstance) { operand = readClassInstance(token); } Para dar suporte à leitura do operador , adicionamos a condição correspondente para o operador atual na pilha do operador: NestedClassInstance if (!operators.isEmpty() && (operators.peek() == Operator.ClassInstance || operators.peek() == Operator.NestedClassInstance)) { operand = readClassInstance(token); } O método 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 como uma expressão de operando inteira. readClassInstance() ClassExpression 3. Conclusão Tudo está pronto agora. Nesta parte, implementamos classes aninhadas como mais uma etapa para criar uma linguagem de programação completa.