If you are on time, and have a desire to learn something cool, you can create a parser of expressions very simple for this.
A good recipe to follow in a parser is to create the read methods like this:
DadoDeRetorno LerAlgumaCoida(string codigo, ref int pos)
{
// lê algo do código e avança a posição de leitura
}
That one DadoDeRetorno
depends on what you want to do.
interpret: in that case the output of most methods is the result obtained from the interpretation. As you will not know beforehand the type to be returned, so it will be object
even.
generate syntactic tree: then the output will be a syntactic tree node. This is the case for example, you want to use LINQ Expression Trees, which you can then compile, which will give you a great performance gain.
compile: few languages can be compiled directly from code, but an expression parser is probably an exception. In this case, you probably want to return a string/array already with the translation to the target language.
For every thing that is successfully handled, you should update the variable pos
.
In addition, the return of each function should indicate whether or not the method was successful to facilitate things.
You will need methods like this model to read spaces, to read variable names, to read numbers, among others:
bool LerEspacos(string codigo, ref int pos); // retorna false se não ler nada
string LerNomeDeVariavel(string codigo, ref int pos); // retorna null se não ler nada
int? LerNumeroInteiro(string codigo, ref int pos); // retorna null se não ler nada
Each can also receive other values relevant to each type of parser.
For example, when making an interpreter, you need to pass the values of the variables. If you are a compiler, you may need to pass a list of the registrars in use and maybe even change this list.
You will also need a method for processing expressions. This goes into the same model, but within it, the implementation is a little bit more complicated.
Expression LerExpressao(string codigo, ref int pos, Contexto contexto);
For this case you need two stacks, one of operators, and the other of operands.
Whenever an operand is found, you stack it in the list of operands.
When an operator is found, you need to stack it into the stack of operators, but only if it has greater precedence than what is already in the stack. Otherwise, it is necessary to perform the operation that is in the stack with the operands that are in the operand stack.
Example of the steps of a two-stack interpreter:
Entrada: 1 + 3 * 5 + 8
// Lê 1 da entrada e põe na pilha de operandos
Operandos: 1
Operadores:
Entrada: + 3 * 5 + 8
// Lê "+" da entrada e põe na pilha de operadores
Operandos: 1
Operadores: +
Entrada: 3 * 5 + 8
// Lê 3 da entrada e põe na pilha de operandos
Operandos: 1 3
Operadores: +
Entrada: * 5 + 8
// Lê "*" da entrada e põe na pilha de operadores
Operandos: 1 3
Operadores: + *
Entrada: 5 + 8
// Lê 5 da entrada e põe na pilha de operandos
Operandos: 1 3 5
Operadores: + *
Entrada: + 8
// Não pode empilhar + sobre *
// Executa a operação 3 * 5 = 15
// Remove o operador * da pilha
// Remove os operandos 3 e 5 da pilha
// Adiciona o 15 e o + nas respectivas pilhas
Operandos: 1 15
Operadores: + +
Entrada: 8
Operandos: 1 15 8
Operadores: + +
// Acabou, agora basta ir executando todos os itens das pilhas
// 15 + 8 = 23
Operandos: 1 23
Operadores: +
// 1 + 23 = 24
Operandos: 24
Operadores:
// o resultado é o que sobra na pilha de operandos = 24