%%{init: {"flowchart": {"useMaxWidth": false}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
A["Texto que alguém escreveu<br/>uma descrição, um programa"] --> B["Análise léxica<br/>letras viram palavras da linguagem"]
B --> C["Análise sintática<br/>palavras viram uma árvore"]
C --> D["Análise semântica<br/>verifica o significado do que foi escrito"]
D --> E["Geração de código<br/>a árvore vira o objeto"]
E --> F["Um resultado<br/>sobre entrada que ninguém preparou"]
B -.-> G["Autômatos finitos"]
C -.-> H["Gramáticas livres de contexto"]
G -.->|"o que nenhum autômato finito reconhece"| H
Introdução
Por onde começar
Cada assunto aparece aqui como um módulo, e cada módulo como uma lista de páginas. Todas tratam do mesmo assunto e servem a momentos diferentes do estudo. O guia é um texto curto, que escolhe as ideias centrais do módulo. O material ensina o módulo inteiro e se entende sozinho. As outras páginas se consultam conforme a pergunta que você traz.
Quem está começando segue a lista de módulos na ordem. Quem chega com uma dúvida abre o módulo do assunto e escolhe a página pela pergunta que traz. As páginas de um módulo não fazem o mesmo trabalho. Se a página que você abre não serve à sua pergunta, você gasta tempo de leitura e não encontra a resposta.
O que você programa ao longo dos módulos se confere quando é executado. Um padrão é a descrição de uma forma de texto, como “uma letra seguida de dígitos”. Um programa que recebe um texto e decide se ele tem a forma descrita se chama reconhecedor. Se o reconhecedor aceita um texto que devia recusar, o erro aparece assim que ele recebe esse texto, sem depender da opinião de ninguém. Por isso, execute cada programa que você escrever.
As páginas de um módulo
Qual página abrir depende de uma pergunta: você está aprendendo o assunto agora, voltando a algo já lido ou conferindo se aprendeu? Quem está aprendendo começa pelo guia, que abre a lista de todo módulo e se lê de uma vez, do começo ao fim. O guia escolhe do módulo no máximo três ideias e mostra um exemplo executado passo a passo. Perto do fim vêm três perguntas sobre casos que o guia não resolveu, chamadas previsões:
- na primeira, você segue à mão um procedimento ou uma máquina, passo a passo, sobre um caso, e diz o que sai;
- na segunda, você diz o que é preciso construir para atender a um pedido;
- na terceira, você diz se uma coisa é garantida, se é impossível, ou o que separa dois casos.
A resposta de cada previsão fica guardada num bloco que só abre quando você pede. Escreva a sua resposta antes de abrir o bloco. Se ela estiver errada, ainda serve: compare-a com a resposta do bloco e ache o ponto em que as duas se separam. O que o guia deixou de fora está no material, a página que ensina o módulo inteiro. A última seção do guia, “Para ir além”, cita pelo título as seções do material onde está o que ficou de fora.
O material não deixa de fora nenhuma definição do assunto, e se entende sozinho: foi escrito para quem está lendo o assunto pela primeira vez. Entre as páginas do módulo, só o material explica o porquê da teoria: por que cada coisa é assim, e não de outro jeito.
Um algoritmo é um procedimento escrito passo a passo, e no material cada algoritmo vem numa forma que você consegue transformar em programa. O raciocínio que mostra por que uma afirmação vale se chama demonstração, e a afirmação que tem demonstração se chama teorema. O material faz a demonstração por inteiro quando ela cabe no nível do texto. Quando uma coisa podia ser feita de outro jeito, e esse jeito foi descartado, o material diz qual é e por quê. O material é a página para entender o que o guia apontou, e para ir além do guia.
O resumo traz o mesmo assunto em menos palavras, para quem já leu o material. As definições e os teoremas aparecem com as mesmas palavras do material, mas a demonstração de cada teorema encolhe a um parágrafo, e o passo a passo fica no material. O resumo serve para revisar o que já foi estudado, pouco antes de você voltar ao seu código, e código é o texto de um programa. O resumo não é primeira leitura: nele, cada teorema chega sem o passo a passo da demonstração.
Veja um caso. Um reconhecedor pode ser construído como uma máquina que lê o texto um símbolo por vez, e um símbolo é uma letra, um dígito ou um sinal. A cada símbolo a máquina passa de uma situação a outra, e cada situação se chama estado. Uma máquina assim, com um número fixo de estados, é um autômato finito. A máquina é determinística quando, de cada estado, para cada símbolo lido, ela tem um único estado seguinte. Entre as máquinas determinísticas que servem de reconhecedor para um mesmo padrão, a que tem menos estados se chama autômato mínimo. Duas máquinas mínimas para o mesmo padrão só diferem nos nomes dos estados, e por isso se diz que o autômato mínimo é único a menos de renomeação de estados. Quem começa pelo resumo pode sair sabendo disso sem ter visto a demonstração, que só o material faz passo a passo.
O lugar da página Treino, na lista, é logo depois do guia. Essa página é opcional e serve para treinar a memória depois de estudar o módulo. Ela tem três partes. Na primeira, a revisão, você escreve ou desenha de memória o que lembra do módulo, responde a perguntas sobre ele e confere cada resposta num bloco que só abre quando você pede. A segunda são os cartões. Eles trazem as definições e os teoremas do módulo e os termos do glossário que o módulo ensina. A frente de cada cartão mostra o nome, e o verso mostra o enunciado. Tente lembrar o enunciado antes de abrir o cartão: quem lê o verso direto só faz mais uma releitura.
A terceira parte do Treino é um texto pronto, que você copia e cola como primeira mensagem de uma conversa nova com um assistente de inteligência artificial. Com esse texto, o assistente passa a responder com base no guia, nos cartões e na revisão do módulo, e não dá a resposta antes de você tentar.
Os exercícios servem a quem está conferindo se aprendeu. São três, em dificuldade crescente, e tratam só da teoria daquele assunto. Nenhum exercício se apoia no código que você escreve, nem no código da página que fecha a lista. Essa página é o Projeto do Professor. Um compilador é um programa que reescreve outro programa na forma que o computador executa. Essa reescrita é uma tradução, feita em tarefas sucessivas, e cada tarefa se chama fase. O Projeto do Professor mostra um compilador completo, construído módulo a módulo como modelo. Esse compilador é o projeto de referência. Em cada módulo, a página Projeto do Professor traz o código desse compilador como ele fica ao fim daquele módulo, com as decisões que o moldaram e as alternativas descartadas.
Um recurso acompanha o módulo como arquivo, fora da lista de páginas, e é opcional: os slides, em PDF. Eles resumem a teoria do material em tópicos e mostram o código do Projeto do Professor trecho a trecho, com a decisão de cada trecho ao lado.
Quatro páginas servem ao percurso inteiro e ficam fora das listas dos módulos:
- o Conteúdo Programático traz o mapa do percurso, com o nível de profundidade de cada módulo;
- o Projeto Integrador descreve o sistema que você constrói enquanto lê e as propriedades que a linguagem dele precisa ter;
- a Referência de tecnologias cobre a linguagem de programação e as ferramentas usadas no código;
- o Glossário dá a definição do termo que chegou sem explicação.
A Referência de tecnologias trata do C++20, da biblioteca padrão e do CMake. O C++20 é a linguagem de programação em que o código é escrito. A biblioteca padrão é o conjunto de trechos de programa prontos que vem junto com a linguagem. O CMake é a ferramenta que descreve como o seu código é transformado em programa que o computador executa. A página foi escrita para consulta com o seu código aberto ao lado.
O que você constrói lendo
Enquanto lê os módulos, você constrói um sistema seu, o Projeto Integrador. Esse sistema é o compilador de uma linguagem que você define. Você escreve o compilador em C++20. A linguagem que ele traduz é outra, e é você quem fixa as regras de escrita dela. Você também escolhe o domínio dessa linguagem, isto é, o assunto de que ela trata, como caracteres de um texto ou eventos registrados. Quem escreve nessa linguagem descreve padrões. O compilador transforma os padrões num arquivo, chamado objeto. Depois, outro programa do sistema executa esse objeto sobre dados reais do domínio, e os dados que um programa recebe são a entrada dele. A página do Projeto Integrador traz, na seção “O que a Sua Linguagem Precisa Ter”, as propriedades que a sua escolha de domínio precisa ter.
O projeto de referência se chama Peneira. A Peneira compila uma pequena linguagem de padrões sobre texto e executa o objeto que gera. Ela tem nove componentes. Um deles lê o texto de um padrão e aceita ou recusa o que foi digitado: vamos chamá-lo de leitor de padrões. Outro é o programa que executa o objeto gerado, chamado máquina virtual. A Peneira não é o seu sistema, e as escolhas dela são outras. Ela serve de modelo: mostra como fica um resultado bem acabado.
O seu compilador e a Peneira têm as mesmas fases, que estão no diagrama. Nele, duas linhas tracejadas saem das duas primeiras fases e chegam à teoria que sustenta cada uma. A teoria da primeira fase é a dos autômatos finitos. A teoria da segunda fase é a das gramáticas livres de contexto. Uma gramática é um conjunto de regras que diz como se formam os textos de uma linguagem. A terceira linha tracejada vai dos autômatos finitos às gramáticas: há formas de texto que nenhum autômato finito reconhece.
A análise sintática é a fase do diagrama em que as palavras viram uma árvore, a estrutura em forma de organograma em que o compilador guarda o texto que leu. O programa que faz essa fase se chama analisador sintático. Essa fase pode ser feita por duas estratégias: a descendente monta a árvore de cima para baixo, e a ascendente monta a árvore de baixo para cima. A Peneira usa a descendente, com o analisador escrito inteiro à mão.
Em dois módulos a Peneira não ganha código novo, e o motivo é diferente em cada um:
- no módulo “Autômatos de pilha”, o assunto é um modelo de máquina, e o código que realiza esse modelo é o do analisador descendente, que tem módulo próprio;
- no módulo “Análise sintática ascendente”, a estratégia ascendente é estudada por comparação com a descendente, para formar critério de escolha entre as duas, e fica fora do código porque a Peneira usa a descendente.
Existem programas que geram um analisador ascendente pronto. No módulo da análise ascendente, eles entram para comparação, nunca como ferramenta que constrói a Peneira.
Cada fase do compilador recebe o que a fase anterior produziu. Na Peneira, a análise léxica separa o texto em palavras da linguagem, e o programa que faz essa fase é o analisador léxico. O analisador léxico é montado com o componente que traduz padrões em autômatos finitos, as máquinas que decidem se um texto tem a forma que um padrão descreve. Esse componente é construído nos módulos de autômatos finitos. No seu sistema, o analisador léxico também depende desse componente, e a página do Projeto Integrador põe essa dependência entre as propriedades exigidas. Se você deixar esse componente para depois, a análise léxica não tem sobre o que se apoiar. Se o componente não tiver teste próprio, um defeito nele só aparece na análise léxica, longe do lugar onde nasceu.
Uma rotina que funciona
Em cada módulo, siga esta ordem:
- Leia o guia e responda às três previsões.
- Leia o material, pelas seções que o guia aponta em “Para ir além”. Se você errou uma previsão, comece pela seção que trata do ponto errado.
- Ao chegar a um algoritmo, pare de ler e escreva o algoritmo como programa antes de seguir.
- Com o programa escrito e compilando sem erro, teste o programa com casos que você inventou à mão, com a resposta certa de cada caso escrita antes de executar.
- Faça os exercícios, que são de teoria.
O passo 3 existe porque quem acompanha um algoritmo bem explicado ainda não sabe se consegue escrevê-lo, e só descobre tentando. No passo 4, não use casos produzidos por outro código seu, escrito a partir da mesma regra que o programa em teste: esses casos herdam o engano que você queria encontrar. Se o gerador de casos e o programa em teste partem da mesma regra errada, os dois concordam, e o teste passa. Os exercícios ficam por último porque medem se o entendimento continua firme depois de você escrever o programa.
Guarde, desde o começo, um registro curto das decisões que você tomou e das alternativas que descartou, com a razão de cada uma. Quando você chegar à análise semântica, a fase que verifica o significado do que foi escrito, o seu compilador vai percorrer uma árvore cuja forma você decidiu antes, na análise sintática. Nesse ponto, você pode já não lembrar a razão daquela forma. Com o registro, a razão está escrita; sem o registro, você tem de reconstruí-la a partir do código.
E na volta de um intervalo longo, por onde recomeçar? Comece pelo resumo do módulo em que você parou, ou pelos cartões do Treino, que pedem o enunciado antes de mostrá-lo. Depois, execute e altere um pouco o último componente do seu sistema que já funcionava. Recomeçar relendo a teoria desde o início também funciona, mas o seu programa continua estranho para você até você voltar a mexer nele. Quando você altera o código e executa, o resultado mostra na hora se o programa faz o que você esperava, e também se a teoria ficou entendida pela metade.