Parte 6! Chegamos!
E dessa vez fizemos algo um pouco diferente: fizemos uma live mostrando a implementação do nosso famigerado while
! Ao vivo, a cores e em Full HD!
Saca só!!!
Essa, aliás, foi a segunda live sobre o tema. Antes dela, fiz uma para explicar um pouco sobre o processo de criação do compilador e tirar dúvidas. Caso queira assistir, clique aqui.
De qualquer forma, vou repassar a implementação detalhando cada ponto importante…
Bom, vamos ao que viemos.
Como sempre, a implementação de uma nova funcionalidade passa pela criação e extração dos tokens. Para quem acompanha a série, já sabe e de cor e salteado como fazer isso:
Arquivo CodeAnalysis/TokenType.cs:
public enum TokenType
{
...
While,
...
}
Começamos adicionando um novo item de enum
chamado While
. Em seguida, vamos modificar a classe Token.cs:
Arquivo CodeAnalysis/Token.cs:
public class Token
{
...
public const string WHILE = "while";
...
public static Token While(int position)
=> new(TokenType.While, WHILE, position);
...
}
Com isso, já podemos começar a extração do token while
.
Arquivo CodeAnalysis/Token.cs:
public class Lexer
{
...
private Token ExtractKeyword()
{
...
if (identifier.Value == Token.WHILE)
return Token.While(position);
return Token.Identifier(position, identifier.Value);
}
...
}
É isso. O processo é simples, já que fizemos toda a estrutura de código anteriormente para suportar tokens como if
, else
, etc.
Primeira parte foi para o done!
Arquivo CodeAnalysis/SyntaxParser.cs:
No post passado, na parte 5, ao implementarmos suporte a if
e else
, adicionamos dois métodos importantes que vão ser reutilizados na implementação do while
: ParseBlock
e SkipBlockUntil
.
Com isso, a implementação do while
acaba sendo razoavelmente simples, até porque a estrutura de execução não difere muito da estrutura de suporte ao if
.
Uma coisa que mostrei ao vivo e acho que é bem legal para “visualizar” o fluxo de validação e evaluate das expressões, foi apresentar o código-fonte e demonstrar como o ponteiro “passeia” entre os tokens, além de destacar o que é uma expressão (algo que vai retornar um valor booleano) e o que é o bloco de execução (trechos de código que serão repetidos n vezes dependendo do resultado computado da expressão).
Vejamos o código:
int i = 1
while i <= 10
print(i)
i = i + 1
end
Quando penso na implementação dos evaluates imagino uma sequência de tokens demarcando o que são expressões e o que são blocos de execução. Dessa forma, fica fácil para eu identificar como vai ficar a estrutura de implementação. Saca só:
int i = 1 while [i <= 10] {print(i) i = i + 1} end
^....................................^
O que temos acima é uma representação linear onde o ^
é um ponteiro que vai avançando entre os tokens até encontrar o token delimitador final. Isso acontece do primeiro ao último token. Aqui, foquei somente no que interessa no momento.
Não sei se isso ficou claro no post anterior, mas esses blocos de execução precisam ter um token de abertura e um de fechamento. Um par que me indica onde começa e onde termina o bloco.
if
eend
,while
eend
e num futurofor
eend
, são blocos com essas características.
Dessa forma, é possível visualizar a expressão (tudo que está entre colchetes), o bloco de execução (tudo que está entre chaves) e onde começa e termina nosso fluxo.
Isso é importante já que temos dois processos fundamentais: EvaluateExpression
e EvaluateToken
.
Lembrando que o último processo da cadeia executada pelo EvaluateExpression
é a invocação do EvaluateToken
, porém, em muitos casos, podemos chamar diretamente o EvaluateToken
.
Por isso essa separação: preciso ter total noção de quando invocar um e quando invocar os outros.
public class SyntaxParser
{
...
private Identifier EvaluateToken()
=> CurrentToken.Type switch
{
...
TokenType.While => EvaluateWhile(),
...
};
private Identifier EvaluateWhile()
{
NextIfTokenIs(TokenType.While);
var whilePosition = _currentPosition;
while (true)
{
var condition = EvaluateExpression();
if (condition.ToBool())
{
ParseBlock(TokenType.End);
_currentPosition = whilePosition;
}
else
{
SkipBlockUntil(TokenType.End);
break;
}
}
NextIfTokenIs(TokenType.End);
return Identifier.None;
}
...
}
EvaluateWhile
! Sim, é simples.
E não, não tem como remover aquele while true
. Segue o jogo…
Começamos validando a presença do token while
e armazenamos a posição atual dele para permitir a repetição do bloco. Lembra do ponteiro ^
? Então… ao chegar no fim do bloco, é necessário voltar para o início, com isso a variável whilePosition
armazena esse valor que será posteriormente setado na variável global _currentPosition
e o processo reinicia do ponto de partida correto.
A cada iteração é necessário processar a expressão (EvaluateExpression
) e validar se o resultado é um valor booleano. Se for booleano e for true
executa o bloco associado (ParseBlock
) e retorna à posição inicial para reavaliar a condição.
Caso a condição seja falsa, o método avança até o token End
, encerrando o laço.
E é aqui que temos uma pegadinha marota: identificar o verdadeiro end
final. Aquele que determina o fim do bloco, já que podemos ter um while
dentro de outro que está dentro de um if
que por sua vez tem um else
. Algo como:
Ou ainda:
int i = 1
while i <= 10
int j = 1
while j <= 10
print(i + "x" + j + "=" + i * j)
j = j + 1
if i % 2 == 0
print(i + " é par")
else
print(i + " é ímpar")
end
end
print("=========")
i = i + 1
end
No exemplo acima, temos 3 tokens do tipo end
, mas qual deles é, de fato, o que finaliza o bloco do while
? E note que existe um while
dentro de um while
.
É aí que entra o famigerado SkipBlockUntil
:
private void SkipBlockUntil(params TokenType[] delimiters)
{
var nestedCount = 0;
while (CurrentToken.Type != TokenType.EndOfFile)
{
if (delimiters.Contains(CurrentToken.Type) && nestedCount == 0)
return;
if (CurrentToken.Type is TokenType.If or TokenType.While)
nestedCount++;
else if (CurrentToken.Type == TokenType.End && nestedCount > 0)
nestedCount--;
_currentPosition++;
}
}
Esse método avança a posição atual dos tokens até encontrar um dos delimitadores informados, ignorando o conteúdo do bloco.
No caso do processamento do if
informamos TokenType.Else
e TokenType.End
. O else
é um delimitador do if
também!
Já no caso do while
informamos somente oTokenType.End
.
Encontrou algum desses tokens, pára o processo!
Tudo isso é feito considerando blocos aninhados, incrementando um contador ao encontrar novos if
ou while
e decrementando ao encontrar end
, garantindo que somente o bloco correto seja pulado.
Feito isso, temos o while
implementado, executando corretamente o bloco de código e saindo graciosamente quando deve sair.
Simples e funcional.
O fato de ter implementado suporte a if
e else
fez com que criássemos métodos que serão reaproveitados em vários outros cenários.
Para o propósito desse compilador, arrisco a dizer que a infraestrutura de código está praticamente completa, sendo necessário somente adicionar suporte a alguns novos tokens, como o for
e um para identificar métodos, algo que ainda não defini como será.
O próximo post promete!
Como foi razoavelmente simples essa implementação, resolvi fazer uma correção na identificação do tipo numérico (double
e int
), reorganizar os testes unitários, incluir vários exemplos de código (verificar números primos e palíndromos, converter decimal para binário, inverter string
, Célsius para Fahrenheit, etc.) além de dar um tapinha na nossa “IDE”.
Aliás, ao reorganizar os testes, eu refiz boa parte deles para poder cobrir o máximo de cenários possíveis.
Hoje são 345 testes, que rodam em aproximadamente 1 segundo(!), dando uma cobertura total de aproximadamente 92%:
O Pug está saindo da casinha \o/
E assim chegamos ao fim da parte 6.
Espero que tenham curtido a live e o post!
Código-Fonte: https://github.com/angelobelchior/Pug.Compiler/tree/parte6
É isso.
Muito obrigado e até a próxima.
Top comments (0)