Como funciona a analise léxica semântica e sintática do compilador? Tem algum site como explica a compilação e o loader?
1. Análise Léxica (Lexer / Scanner)
O que é:
É a primeira etapa do compilador. Serve para transformar o código-fonte em uma sequência de tokens.
O que faz:
- Lê o código como texto bruto (caracteres).
- Identifica palavras-chave, identificadores, números, símbolos, etc.
- Remove espaços em branco, quebras de linha e comentários (não relevantes para a sintaxe).
2. Análise Sintática (Parser)
O que é:
É a segunda etapa. Verifica se os tokens formam uma estrutura gramatical válida segundo a gramática da linguagem de programação.
O que faz:
- Organiza os tokens recebidos da análise léxica em uma árvore sintática (AST - Abstract Syntax Tree).
- Detecta erros de sintaxe, como esquecer um ponto e vírgula, parênteses não fechados, etc.
3. Análise Semântica (Semantic Analysis)
O que é:
É a terceira etapa, depois que a análise sintática confirmou que a estrutura do código é válida.
O que faz:
- Verifica coerência e significado do código.
- Faz checagem de tipos (ex: tentar somar um número com uma string).
- Confirma se variáveis foram declaradas antes de usar.
- Verifica se funções são chamadas com o número correto de argumentos.
- Regras mais “lógicas” da linguagem.
Exemplo Completo de Análise Léxica, Sintática e Semântica
Código-fonte de exemplo (Java):
int contarAte(int limite) {
int contador = 0;
while (contador < limite) {
contador = contador + 1;
}
return contador;
}
1. Análise Léxica (Tokens)
Tipo de Token | Valor |
---|---|
KEYWORD | int |
IDENTIFIER | contarAte |
SYMBOL | ( |
KEYWORD | int |
IDENTIFIER | limite |
SYMBOL | ) |
SYMBOL | { |
KEYWORD | int |
IDENTIFIER | contador |
SYMBOL | = |
NUMBER | 0 |
SYMBOL | ; |
KEYWORD | while |
SYMBOL | ( |
IDENTIFIER | contador |
SYMBOL | < |
IDENTIFIER | limite |
SYMBOL | ) |
SYMBOL | { |
IDENTIFIER | contador |
SYMBOL | = |
IDENTIFIER | contador |
SYMBOL | + |
NUMBER | 1 |
SYMBOL | ; |
SYMBOL | } |
KEYWORD | return |
IDENTIFIER | contador |
SYMBOL | ; |
SYMBOL | } |
2. Análise Sintática (Árvore Sintática - AST)
FunctionDeclaration: contarAte
├── ReturnType: int
├── Parameters:
│ └── int limite
└── Body:
├── VariableDeclaration: int contador = 0
├── WhileStatement:
│ ├── Condition: contador < limite
│ └── Body:
│ └── Assignment: contador = contador + 1
└── ReturnStatement: contador
3. Análise Semântica (Verificações de Significado)
O compilador checa:
O tipo da variável
contador
está correto.
O operador
<
é válido entre dois inteiros.
O incremento de
contador
é válido.
Existe um retorno para a função (obrigatório, pois o tipo de retorno é
int
).
Se tivéssemos erros, exemplos seriam:
Exemplo 1: Comparação de tipos incompatíveis
while (contador < "texto") { ... }
Erro Semântico: Não pode comparar
int
comString
.
Exemplo 2: Uso de variável não declarada
contador = contador + x;
Erro Semântico: Variável
x
não declarada.
Complementando.
A análise semântica normalmente ocorre junto com a sintática. Tem autor inclusive que não gosta de usar o termo análise semântica, preferindo algo como análise de restrições ou análise da semântica estática. As fases descritas pelo @staroski são parte do que é chamado front-end do compilador. O front-end é sempre muito ligado à linguagem fonte (a linguagem de programação que se está compilando). A representação intermediária gerada no front-end, que é a AST, é então passada para o back-end do compilador, que basicamente tem duas outras fases: a geração de código e a otimização.
O back-end é atrelado à linguagem alvo. Na geração de código a AST é percorrida e então gerada a representação do programa em linguagem alvo, seja assembly da arquitetura alvo (C e C++), assembly da uma máquina virtual (Java e C#) ou então uma linguagem de baixo nível intermediária (LLVM) onde vc escolhe a arquitetura alvo e esse assembly intermediário é compilado para o assembly da arquitetura desejada. Por fim, esse código em baixo nível passa pelo último processo para a geração do código objeto, que é de fato o programa compilado e que têm semântica idêntica ao programa fonte. Dependendo da linguagem essas unidades de compilação ainda precisarão ser linkadas com biblitotecas estáticas ou dinâmicas para gerar o executável final.
Já a otimização pode ser feita de várias maneiras, como podas na AST para remoção de código morto, aplicação de redução de força (usar uma instrução de incremento ao invés de se somar um em um registrador), dobragem de constantes (trocar no código o uso de uma constante pelo valor em si), identificação de identidades algébricas (ignorar uma soma com 0 ou multiplicação por 1), eliminação de sub-expressões comuns, código invariante de loops, peephole, entre muitas outras. Podem haver otimizações no próprio assembler que é o software que vai usar o código assembly para gerar o código binário correspondente, ou seja, “trocando” os mnemônicos do assembly para os opcodes do processador. Otimização é talvez a parte mais complicada, porque depende da arquitetura que se tem como alvo.
“There is no such thing as a machine-independent optimization. Not one! People who use the phrase don’t understand the problem! There are lots os semantics-preserving transformations that improve code size or speed for some machines under somes ciscumstances. But those same transformations may be pessimizations for another machine!” (William A. Wulf)
Esta é uma pergunta bem ampla, já que é um tópico bem complexo que envolve muitas áreas de conhecimento diferentes.
Aqui tem um bom resumo (sim, é um resumo), além de vários links para complementar. E lembre-se que tanto este link quanto as respostas acima dão apenas uma pincelada de leve, pois cada coisa citada (parsing, lexing, otimizações, etc) é um mundo à parte, um assunto complexo por si só.
Não estou dizendo isso pra te desencorajar, muito pelo contrário. É só pra vc ter noção do tamanho da encrenca, e não achar que sabe tudo só porque leu meia dúzia de artigos. É um assunto amplo e complexo, e sempre tem algo pra aprender. E com certeza é algo que vai agregar ao seu conhecimento, pois mesmo que vc nunca vá fazer um compilador (e a maioria não vai mesmo, normal), saber como as coisas funcionam nos torna programadores melhores (mesmo que indiretamente).
Por exemplo, um efeito de saber o funcionamento de um compilador é entender porque as linguagens são feitas de determinadas formas, e como estas escolhas resultam em certas características e limitações. Assim vc entende que não existe linguagem perfeita e para de ser aquele fanboy que fica brigando com outros fanboys de outras linguagens