Nesta parte da criação de sua própria linguagem de programação, vamos implementar classes como uma extensão sobre as estruturas definidas anteriormente. Confira as partes anteriores:
O código-fonte completo está disponível
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 na enumeração TokenType para definir todos os tipos de lexemas.
Vejamos o seguinte protótipo de classe e adicionemos as partes regex que faltam aos nossos lexemas TokenType
:
class
na expressão do lexema de palavra- Keyword
para permitir que o analisador léxico saiba onde começa nossa declaração de classe: package org.example.toylanguage.token; ... public enum TokenType { ... Keyword("(if|elif|else|end|print|input|fun|return|loop|in|by|break|next|class)(?=\\s|$)"), ... private final String regex; }
This
como um marcador para uma referência ao objeto atual: public enum TokenType { ... This("(this)(?=,|\\s|$)"); private final String regex; }
Na segunda seção, transformaremos os lexemas recebidos do analisador léxico em enunciados finais seguindo nossas regras linguísticas.
Quando declaramos uma classe ou função, esta declaração deve estar disponível dentro dos limites isolados definidos. Por exemplo, se declararmos uma função chamada turn_on []
na listagem a seguir, ela estará disponível para execução após a declaração:
Mas se declararmos a mesma função dentro do escopo de uma classe, esta função não será mais acessada diretamente do bloco principal:
DefinitionScope
e armazenaremos todas as definições declaradas dentro de dois conjuntos para classes e funções: package org.example.toylanguage.context.definition; public class DefinitionScope { private final Set<ClassDefinition> classes; private final Set<FunctionDefinition> functions; public DefinitionScope() { this.classes = new HashSet<>(); this.functions = new HashSet<>(); } }
Para fornecer essa capacidade, adicionaremos a instância pai DefinitionScope
como uma referência à camada superior, que usaremos para subir até a camada de definição superior.
public class DefinitionScope { private final Set<ClassDefinition> classes; private final Set<FunctionDefinition> functions; private final DefinitionScope parent; public DefinitionScope(DefinitionScope parent) { this.classes = new HashSet<>(); this.functions = new HashSet<>(); this.parent = parent; } }
public class DefinitionScope { … public ClassDefinition getClass(String name) { Optional<ClassDefinition> classDefinition = classes.stream() .filter(t -> t.getName().equals(name)) .findAny(); if (classDefinition.isPresent()) return classDefinition.get(); else if (parent != null) return parent.getClass(name); else throw new ExecutionException(String.format("Class is not defined: %s", name)); } public void addClass(ClassDefinition classDefinition) { classes.add(classDefinition); } public FunctionDefinition getFunction(String name) { Optional<FunctionDefinition> functionDefinition = functions.stream() .filter(t -> t.getName().equals(name)) .findAny(); if (functionDefinition.isPresent()) return functionDefinition.get(); else if (parent != null) return parent.getFunction(name); else throw new ExecutionException(String.format("Function is not defined: %s", name)); } public void addFunction(FunctionDefinition functionDefinition) { functions.add(functionDefinition); } }
java.util.Stack
(LIFO): package org.example.toylanguage.context.definition; public class DefinitionContext { private final static Stack<DefinitionScope> scopes = new Stack<>(); public static DefinitionScope getScope() { return scopes.peek(); } public static DefinitionScope newScope() { return new DefinitionScope(scopes.isEmpty() ? null : scopes.peek()); } public static void pushScope(DefinitionScope scope) { scopes.push(scope); } public static void endScope() { scopes.pop(); } }
Nesta seção, abordaremos o MemoryScope
para gerenciar variáveis de classe e função.
Mas se declararmos uma variável dentro de uma função ou classe, a variável não estará mais disponível no bloco de código principal (superior):
Para implementar essa lógica e armazenar variáveis definidas em um escopo específico, criamos a classe MemoryScope
que conterá um mapa com o nome da variável como chave e a variável Value
como valor:
public class MemoryScope { private final Map<String, Value<?>> variables; public MemoryScope() { this.variables = new HashMap<>(); } }
DefinitionScope
, fornecemos acesso às variáveis de escopo do pai: public class MemoryScope { private final Map<String, Value<?>> variables; private final MemoryScope parent; public MemoryScope(MemoryScope parent) { this.variables = new HashMap<>(); this.parent = parent; } }
MemoryScope
superior: public class MemoryScope { ... public Value<?> get(String name) { Value<?> value = variables.get(name); if (value != null) return value; else if (parent != null) return parent.get(name); else return NullValue.NULL_INSTANCE; } public void set(String name, Value<?> value) { MemoryScope variableScope = findScope(name); if (variableScope == null) { variables.put(name, value); } else { variableScope.variables.put(name, value); } } private MemoryScope findScope(String name) { if (variables.containsKey(name)) return this; return parent == null ? null : parent.findScope(name); } }
set
e get
, adicionamos mais duas implementações para interagir com a camada atual (local) do MemoryScope
: public class MemoryScope { ... public Value<?> getLocal(String name) { return variables.get(name); } public void setLocal(String name, Value<?> value) { variables.put(name, value); } }
Esses métodos serão usados posteriormente para inicializar argumentos de função ou argumentos de instância de classe. Por exemplo, se criamos uma instância da classe Lamp
e passamos a variável de type
global pré-definida, esta variável não deve ser alterada quando tentamos atualizar a propriedade lamp_instance :: type
:
MemoryContext
usando a coleção java.util.Stack
: package org.example.toylanguage.context; public class MemoryContext { private static final Stack<MemoryScope> scopes = new Stack<>(); public static MemoryScope getScope() { return scopes.peek(); } public static MemoryScope newScope() { return new MemoryScope(scopes.isEmpty() ? null : scopes.peek()); } public static void pushScope(MemoryScope scope) { scopes.push(scope); } public static void endScope() { scopes.pop(); } }
Nesta seção, leremos e armazenaremos as definições de classe.
package org.example.toylanguage.statement; public class ClassStatement { }
public class ClassStatement extends CompositeStatement { }
ClassDefinition
para armazenar o nome da classe, seus argumentos, instruções do construtor e o escopo da definição com as funções da classe: package org.example.toylanguage.context.definition; import java.util.List; @RequiredArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class ClassDefinition implements Definition { @EqualsAndHashCode.Include private final String name; private final List<String> arguments; private final ClassStatement statement; private final DefinitionScope definitionScope; }
class
, primeiro precisamos ler o nome da classe e seus argumentos dentro dos colchetes:
package org.example.toylanguage; public class StatementParser { ... private void parseClassDefinition() { Token type = tokens.next(TokenType.Variable); List<String> arguments = new ArrayList<>(); if (tokens.peek(TokenType.GroupDivider, "[")) { tokens.next(TokenType.GroupDivider, "["); //skip opening square bracket while (!tokens.peek(TokenType.GroupDivider, "]")) { Token argumentToken = tokens.next(TokenType.Variable); arguments.add(argumentToken.getValue()); if (tokens.peek(TokenType.GroupDivider, ",")) tokens.next(); } tokens.next(TokenType.GroupDivider, "]"); //skip closing square bracket } } }
Para armazenar essas instruções, criamos uma instância do ClassStatement
definido anteriormente:
private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); }
DefinitionScope
: private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); }
ClassDefinition
e a colocamos no DefinitionContext
: private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); ClassDefinition classDefinition = new ClassDefinition(type.getValue(), arguments, classStatement, classScope); DefinitionContext.getScope().addClass(classDefinition); }
classStatement
até encontrarmos o lexema end
de finalização que deve ser ignorado no final: private void parseClassDefinition() { ... //parse class statements StatementParser.parse(this, classStatement, classScope); tokens.next(TokenType.Keyword, "end"); }
Neste ponto, já podemos ler as definições de classe com instruções e funções do construtor. Agora, vamos analisar a instância da classe:
ClassValue
, que conterá o estado de cada instância de classe. As classes, ao contrário das funções, devem ter um MemoryScope
permanente e esse escopo deve estar disponível com todos os argumentos de instância da classe e variáveis de estado toda vez que interagirmos com a instância da classe: public class ClassValue extends IterableValue<ClassDefinition> { private final MemoryScope memoryScope; public ClassValue(ClassDefinition definition, MemoryScope memoryScope) { super(definition); this.memoryScope = memoryScope; } }
MemoryContext
: public class ClassValue extends IterableValue<ClassDefinition> { private final MemoryScope memoryScope; public ClassValue(ClassDefinition definition, MemoryScope memoryScope) { super(definition); this.memoryScope = memoryScope; } @Override public String toString() { return getValue().getArguments().stream() .map(t -> t + " = " + getValue(t)) .collect(Collectors.joining(", ", getValue().getName() + " [ ", " ]")); } public Value<?> getValue(String name) { Value<?> result = MemoryContext.getScope().getLocal(name); return result != null ? result : NULL_INSTANCE; } public void setValue(String name, Value<?> value) { MemoryContext.getScope().setLocal(name, value); } }
MemoryScope#getLocal()
e MemoryScope#setLocal()
, trabalhamos com a camada atual de variáveis MemoryScope
. Mas antes de acessar o estado da instância da classe, precisamos colocar seu MemoryScope
no MemoryContext
e liberá-lo quando terminarmos: public class ClassValue extends IterableValue<ClassDefinition> { ... public Value<?> getValue(String name) { MemoryContext.pushScope(memoryScope); try { Value<?> result = MemoryContext.getScope().getLocal(name); return result != null ? result : NULL_INSTANCE; } finally { MemoryContext.endScope(); } } public void setValue(String name, Value<?> value) { MemoryContext.pushScope(memoryScope); try { MemoryContext.getScope().setLocal(name, value); } finally { MemoryContext.endScope(); } } }
ClassExpression
restante que será usada para construir instâncias de classe definidas durante a análise de sintaxe. Para declarar a definição da instância da classe, fornecemos a ClassDefinition
e a lista de argumentos Expression
que serão transformados nas instâncias finais do Value
durante a execução da instrução: package org.example.toylanguage.expression; @RequiredArgsConstructor @Getter public class ClassExpression implements Expression { private final ClassDefinition definition; private final List<Expression> arguments; @Override public Value<?> evaluate() { ... } }
Expression#evaluate()
que será utilizado durante a execução para criar uma instância do ClassValue
definido anteriormente. Primeiro, avaliamos os argumentos Expression
nos argumentos Value
: @Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = arguments.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }
@Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = arguments.stream().map(Expression::evaluate).collect(Collectors.toList()); //get class's definition and statement ClassStatement classStatement = definition.getStatement(); //set separate scope MemoryScope classScope = new MemoryScope(null); MemoryContext.pushScope(classScope); try { ... } finally { MemoryContext.endScope(); } }
ClassValue
e escrevemos os argumentos Value
da classe no escopo de memória isolado: try { //initialize constructor arguments ClassValue classValue = new ClassValue(definition, classScope); 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)); ... } finally { MemoryContext.endScope(); }
Observe que transformamos os argumentos Expression
nos argumentos Value
antes de configurar um MemoryScope
vazio. Caso contrário, não conseguiremos acessar os argumentos de instância da classe, por exemplo:
ClassStatement
. Mas antes disso, devemos definir o DefinitionScope
da classe para poder acessar as funções da classe nas instruções do construtor: 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(); }
9*. Uma última coisa, podemos tornar as classes mais flexíveis e permitir que um usuário crie instâncias de classe antes de declarar a definição de classe:
Isso pode ser feito delegando a inicialização de ClassDefinition
para o DefinitionContext
e acessando-o somente quando avaliamos uma expressão:
public class ClassExpression implements Expression { private final String name; private final List<Expression> arguments; @Override public Value<?> evaluate() { //get class's definition and statement ClassDefinition definition = DefinitionContext.getScope().getClass(name); ... } }
Você pode fazer a mesma delegação para FunctionExpression invocar funções antes da definição.
ExpressionReader
. Não há diferença entre as instâncias da estrutura previamente definidas . Precisamos apenas ler os argumentos Expression
e construir a ClassExpression
: public class ExpressionReader ... // read class instance: new Class[arguments] private ClassExpression readClassInstance(Token token) { List<Expression> arguments = new ArrayList<>(); if (tokens.peekSameLine(TokenType.GroupDivider, "[")) { tokens.next(TokenType.GroupDivider, "["); //skip opening square bracket while (!tokens.peekSameLine(TokenType.GroupDivider, "]")) { Expression value = ExpressionReader.readExpression(this); arguments.add(value); if (tokens.peekSameLine(TokenType.GroupDivider, ",")) tokens.next(); } tokens.next(TokenType.GroupDivider, "]"); //skip closing square bracket } return new ClassExpression(token.getValue(), arguments); } }
Neste momento, podemos criar uma classe e executar as instruções do construtor da classe. Mas ainda não conseguimos executar as funções da classe.
FunctionExpression#evaluate
que aceitará ClassValue
como uma referência à instância da classe da qual queremos invocar uma função: package org.example.toylanguage.expression; public class FunctionExpression implements Expression { ... public Value<?> evaluate(ClassValue classValue) { } }
Expression
nos argumentos Value
usando o MemoryScope
atual: public Value<?> evaluate(ClassValue classValue) { //initialize function arguments List<Value<?>> values = argumentExpressions.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }
MemoryScope
e o DefinitionScope
da classe para o contexto: ... //get definition and memory scopes from class definition ClassDefinition classDefinition = classValue.getValue(); DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope(); //set class's definition and memory scopes DefinitionContext.pushScope(classDefinitionScope); MemoryContext.pushScope(memoryScope);
Value
avaliados: public Value<?> evaluate(ClassValue classValue) { //initialize function arguments List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList()); //get definition and memory scopes from class definition ClassDefinition classDefinition = classValue.getValue(); DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope(); MemoryScope memoryScope = classValue.getMemoryScope(); //set class's definition and memory scopes DefinitionContext.pushScope(classDefinitionScope); MemoryContext.pushScope(memoryScope); try { //proceed function return evaluate(values); } finally { DefinitionContext.endScope(); MemoryContext.endScope(); } }
::
. Atualmente, este operador é gerenciado pela implementação ClassPropertyOperator
( StructureValueOperator ), que é responsável por acessar as propriedades da classe:
Vamos melhorá-lo para oferecer suporte a invocações de função com os mesmos dois pontos duplos ::
caractere:
A função da classe pode ser gerenciada por este operador somente quando a expressão da esquerda é a ClassExpression
e a segunda é a FunctionExpression
:
package org.example.toylanguage.expression.operator; public class ClassPropertyOperator extends BinaryOperatorExpression implements AssignExpression { public ClassPropertyOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue) { if (getRight() instanceof VariableExpression) { // access class's property // new ClassInstance[] :: class_argument return ((ClassValue) left).getValue(((VariableExpression) getRight()).getName()); } else if (getRight() instanceof FunctionExpression) { // execute class's function // new ClassInstance[] :: class_function [] return ((FunctionExpression) getRight()).evaluate((ClassValue) left); } } throw new ExecutionException(String.format("Unable to access class's property `%s``", getRight())); } @Override public void assign(Value<?> value) { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue && getRight() instanceof VariableExpression) { String propertyName = ((VariableExpression) getRight()).getName(); ((ClassValue) left).setValue(propertyName, value); } } }
Nesta parte, implementamos classes. Agora podemos criar coisas mais complexas, como implementação de pilha:
main [] fun main [] stack = new Stack [] loop num in 0..5 # push 0,1,2,3,4 stack :: push [ num ] end size = stack :: size [] loop i in 0..size # should print 4,3,2,1,0 print stack :: pop [] end end class Stack [] arr = {} n = 0 fun push [ item ] this :: arr << item n = n + 1 end fun pop [] n = n - 1 item = arr { n } arr { n } = null return item end fun size [] return this :: n end end