This content originally appeared on DEV Community and was authored by Angelo Belchior
Parte 7! Chegamos!
Habemus for
!
E mais uma vez implementamos uma funcionalidade ao vivo! Gostei desse formato.
Agora é possível escrever códigos como esse:
for int i = 1 to 10 step 2
print(1)
end
Porém, se você não assistiu, não tem problema: vou repassar o código de ponta a ponta para que você não perca nada.
Então, vamos ao que viemos.
Se você nos acompanha, já sabe que começamos sempre fazendo o mapeamento dos tokens:
Pasta CodeAnalysis:
public enum TokenType
{
..
For,
To,
Step,
...
}
public class Token
{
...
public const string FOR = "for";
public const string TO = "to";
public const string STEP = "step";
...
public static Token For(int position)
=> new(TokenType.For, FOR, position);
public static Token To(int position)
=> new(TokenType.To, TO, position);
public static Token Step(int position)
=> new(TokenType.Step, STEP, position);
}
Basicamente, adicionamos novos itens de enum que mapeiam os novos tokens: for
, to
e step
.
Agora é extrair do código:
public class Lexer
{
private Token ExtractKeyword()
{
...
if (identifier.Value == Token.FOR)
return Token.For(position);
if (identifier.Value == Token.TO)
return Token.To(position);
if (identifier.Value == Token.STEP)
return Token.Step(position);
return Token.Identifier(position, identifier.Value);
}
}
Sem segredo e sem suar…
Agora precisamos alterar nossa classe SyntaxParser
. O mais interessante é que, após muitas implementações, temos basicamente um padrão a ser seguido para adicionar uma funcionalidade nova.
Na live fiz questão de mostrar novamente como penso no momento de implementação de uma nova feature.
Analise o código abaixo:
for int i = 1 to 10 step 2
print(1)
end
Para que eu “visualize” o fluxo de implementação, gosto de escrever o código dessa forma:
$for [int i =] {1} to {10} $step {2} < print(1) > $end
.......................................^............^
|<-<-<-<-<-<-|
Explicando:
- O que começa com $ são tokens que preciso validar a sua existência (caso seja obrigatório) e sua posição no código.
- O que está entre colchetes é um evaluate interno, isto é, eu valido os tokens de declaração de variável e jogo na memória global (OK, não é a forma ideal, mas isso vai mudar num futuro) .
- O que está entre { } é uma expressão e, sendo assim, vamos aplicar o
EvaluateExpression
. Dessa forma, podemos colocar qualquer expressão válida para calcularmos o valor. - O que está entre < > é um processo que vai ser executado inúmeras vezes, e para isso vamos invocar o método
ParseBlock
que por sua vez efetua umEvaluateToken
caso o token atual não seja um token delimitador de bloco.
Basicamente é isso. E caso você não tenha entendido o que é um token delimitador de bloco, explico: um bloco de código é aquele que começa com um determinado token e termina com end
ou – no caso do if
– else
.
if -> end
if -> else -> end
while -> end
for -> end
Sendo, assim, precisamos saber exatamente quando existiu uma abertura e um fechamento de bloco. Falei mais sobre isso no post anterior.
Arquivo CodeAnalysis/SyntaxParser.cs:
public class SyntaxParser(Dictionary<string, Identifier> variables, List<Token> tokens)
{
...
private Identifier EvaluateToken()
=> CurrentToken.Type switch
{
...
TokenType.For => EvaluateFor()
...
};
...
private Identifier EvaluateFor()
{
// Valido se o token é o for
NextIfTokenIs(TokenType.For);
/// Extraio a declaração de variável
NextIfTokenIs(TokenType.DataType);
var variableToken = NextIfTokenIs(TokenType.Identifier);
var variableName = variableToken.Value;
// Inicializo o valor da variável que dá início ao for como 1
var from = new Identifier(DataTypes.Int, 1);
// Caso exista uma atribuição na declaração de variável, o evaluate expression é invocado.
if (CurrentToken.Type == TokenType.Assign)
{
NextIfTokenIs(TokenType.Assign);
from = EvaluateExpression();
}
// Seto o valor do from na "memória"
variables[variableName] = from;
// Validamos e aavançamos até o to
NextIfTokenIs(TokenType.To);
// Obtemos o valor informado ao to a partir de uma expressão.
var to = EvaluateExpression();
var end = to.ToInt();
// O step é opcional sendo seu valor inicial 1
var stepValue = 1;
// Caso o step seja informado, o seu valor é obtido a partir de uma expressão.
if (CurrentToken.Type == TokenType.Step)
{
NextIfTokenIs(TokenType.Step);
var step = EvaluateExpression();
stepValue = step.ToInt();
}
// Após obter todos os dados referente a declaração do for, armazeno a posição atual do token. Isso serve para que o looping volte sempre para o lugar correto.
var forPosition = _currentPosition;
// efetuamos o looping (tanto crescente, quanto decrescente)
while (
(stepValue > 0 && variables[variableName].ToInt() <= end) ||
(stepValue < 0 && variables[variableName].ToInt() >= end)
)
{
// Fazemos o parse do corpo do for
ParseBlock();
variables[variableName] = new Identifier(DataTypes.Int, variables[variableName].ToInt() + stepValue);
_currentPosition = forPosition;
}
// Avançamos até sair do for
SkipBlockUntil();
// Validamos se o for termina com o end
NextIfTokenIs(TokenType.End);
return Identifier.None;
}
}
Fiz questão de explicar trecho por trecho no próprio código e acredito que não tem segredo.
O segredo é “visualizar” o trecho de código que será analisado, demarcando cada ponto. Dessa forma, fica muito fácil implementar cada parte já que temos praticamente tudo feito pelo fato de termos implementado if/else
e while
.
É isso. Simples assim.
Curtiu?
Código-fonte está em https://github.com/angelobelchior/Pug.Compiler/tree/parte7
Muito obrigado e até a próxima!
This content originally appeared on DEV Community and was authored by Angelo Belchior