Ciência da Computação

Revisão e Preparação para Prova C1

Professor: Gabriel Soares Baptista

Sumário da Aula

  • Módulo I: Arquitetura Modular e Execução Física (CPU)
  • Módulo II: O Front-end e a Teoria da Análise Léxica
  • Módulo III: Regex, AST, Otimização e Implementação (Lab)

Módulo I: Arquitetura Modular e Execução

Compilador como Tradutor

  • Meta-programação: Escrever código para ler, analisar e transformar outro código.
  • Objetivo: Converter linguagem de alto nível (Python, C) em instruções binárias para o processador.
  • Realidade da Máquina: O compilador segue regras formais para converter a intenção do programador na realidade da máquina.


Por que utilizar uma IR? (Linguagem Intermediária)

  • Abordagem Monolítica ($N \times M$): Um compilador completo para cada par linguagem/arquitetura.
  • Abordagem Modular ($N + M$): Desacopla o processo em Frontend e Backend.
  • Vantagens: Manutenibilidade e otimização centralizada na IR.
CompilerPipelinecluster_frontendsFrontendscluster_backendsBackendscCirIRc->irjavaJavajava->irpythonPythonpython->irx86x86ir->x86armARMir->armmipsMIPSir->mips

A CPU e a Máquina de Estados

  • FSM (Finite State Machine): Modelo matemático com estados limitados e transições previsíveis.
  • Pipeline MIPS (5 etapas): Funciona como uma linha de montagem de fábrica.


Program Counter (PC) e Controle

  • Program Counter (PC): Registrador que armazena o endereço da próxima instrução.
  • Incremento (PC + 4): Em 32 bits, cada instrução (WORD) ocupa 4 bytes ($4 \times 8 = 32$ bits).
  • Desvios (Branches): O PC não segue o incremento se houver um salto (ex: if, loop). Um novo endereço é carregado no PC.

Registradores vs. Máquina de Pilha

  • Arquitetura de Registradores (MIPS): Exige alocação complexa de dados nos 32 registradores disponíveis.
  • Máquina de Pilha (Nossa VM): Simplifica a geração de código usando as operações PUSH e POP.
  • Stack Pointer (SP): Gerencia o topo da pilha de memória.


Questões de fixação - 1 / 3

  1. Para evitar a "multiplicação de esforços" na criação de compiladores para múltiplas linguagens e arquiteturas de hardware, os compiladores modernos fragmentam o processo utilizando uma Linguagem Intermediária (IR). Qual é a principal vantagem matemática e arquitetural dessa abordagem?

  2. Qual é a função do registrador Program Counter, o PC, e por que em várias arquiteturas de 32 bits, como o MIPS, ele é incrementado em 4 bytes a cada instrução sequencial?

  3. Em qual situação o registrador PC não segue o seu incremento automático sequencial e como isso se relaciona com a execução de desvios no código?

  4. Explique a principal diferença de gerenciamento de dados entre uma arquitetura baseada em registradores, como o MIPS, e a arquitetura baseada em pilha que utilizaremos na nossa máquina virtual.

Módulo II: Front-end e Análise Léxica

Etapas do Front-end

  1. Análise Léxica (Morfologia): Converte caracteres em tokens.
  2. Análise Sintática (Gramática): Organiza tokens na Árvore de Sintaxe Abstrata (AST).
  3. Análise Semântica (Sentido): Verifica tipos de variáveis e escopos.

Definições Léxicas: Token, Lexema e Padrão

  • Lexema: Sequência exata de caracteres (ex: while, 42).
  • Padrão (Pattern): Regra de formação (ex: [0-9]+).
  • Token: Estrutura <tipo, valor> (ex: <NUM, 42>, <IDENT, "x">).

Token Padrão (Informal) Exemplos de Lexemas
if caracteres i, f if
number qualquer constante numérica 3, 0, 3.14
id letra seguida por letras/dígitos pi, score

O Scanner sob demanda (get_next_token)

  • Abordagem Orientada à Demanda: O Parser (Sintático) solicita o próximo token ao Scanner (Léxico).

  • Eficiência: Se houver um erro na linha 1, o compilador para imediatamente sem processar o resto do arquivo de 1 milhão de linhas.


LexerDemandLexLexerSinParserLex->SintokenSin->Lexget_next_token()

Engenharia de Entrada: Buffering

  • Pares de Buffers: Divide a memória em duas metades. Carrega a segunda sem apagar a primeira se o lexema cruzar a borda.

  • Problema da Fronteira: Um lexema (ex: while) pode ser cortado ao meio entre duas leituras de disco.

  • Sentinelas: Caracteres especiais no fim do buffer para evitar checar o fim do arquivo a cada byte lido.


Questões de fixação - 2 / 3

  1. Sobre as definições e o funcionamento do analisador léxico (Scanner), classifique: O lexema é a sequência exata e literal de caracteres encontrada no código-fonte, enquanto o token é a estrutura de dados que categoriza esse lexema. (V/F)

  2. Explique como a abordagem orientada à demanda economiza recursos computacionais no caso de um erro de sintaxe encontrado logo no início de um arquivo de código extenso.

  3. O que é o "Problema da Fronteira do Buffer" e como ele é resolvido na etapa de análise léxica utilizando a técnica de Pares de Buffers?

  4. A Tabela de Símbolos permite duplicatas de identificadores para economizar memória? Como o Scanner lida com identificadores repetidos (ex: variável a usada três vezes)?

Módulo III: Regex, AST e Implementação

Fundamentos de Expressões Regulares (Regex)

  • União (|): Escolha (a | b).
  • Concatenação: Sequência (ab).
  • Kleene (*): Zero ou mais ocorrências.
  • Positivo (+): Uma ou mais ocorrências (aa*).
  • Opcional (?): Zero ou uma ocorrência.
  • Classes ([]): Conjunto ([a-zA-Z]).
  • Negação ([^]): Exceto ([^"]).
  • Quantidade ({n}): Exatamente n ocorrências (a{2}).

Regex na Prática: Identificadores e Números

  • Identificador: Deve começar com letra, seguido de letras ou dígitos.

  • [a-zA-Z][a-zA-Z0-9]*

  • Número Inteiro: Sequência de um ou mais dígitos.

  • [0-9]+

  • Número Decimal (Float):

  • [0-9]+\.[0-9]+

  • Hexadecimal (C): Começa com 0x seguido de 0-9 ou a-f.

  • 0x[0-9a-fA-F]+

AST e Constant Folding (Otimização)

  • AST (Abstract Syntax Tree): Representação hierárquica do código.

  • Constant Folding (Dobra de Constantes): O compilador calcula expressões constantes em tempo de compilação.

Antes: x = 2 * 3 + a

AST error: name 'hashlib' is not defined

Depois: x = 6 + a

AST error: name 'hashlib' is not defined

Implementação em C: X-Macros

  • Princípio DRY (Don't Repeat Yourself): Evita definir tokens no enum e no array de strings separadamente.

  • Sincronização: Uma única lista TOKEN_LIST gera ambos automaticamente.

#define TOKEN_LIST \
    X(TOK_PLUS) X(TOK_MINUS) X(TOK_IDENTIFIER)

typedef enum {
#define X(name) name,
    TOKEN_LIST
#undef X
} TokenType;

static const char* const TokenNames[] = {
#define X(name) #name,
    TOKEN_LIST
#undef X
};

Implementação: Two-Pointers e Lookahead

  • Estratégia Two-Pointers:

    • start: Início do lexema atual.
    • current: Cursor de leitura avançando.
  • Lookahead (peek e peek_next): Espia o caractere atual e o próximo sem consumi-los, respectivamente. Essencial para diferenciar / de // ou = de ==.

  • Função match: Verifica se o próximo caractere é o esperado. Se sim, consome e retorna true.

Questões de fixação - 3 / 3

  1. Considere a expressão int x = 2 * 3 + a;. Descreva como o processo de otimização via Constant Folding alteraria a AST dessa expressão e qual o benefício para o programa final.

  2. Escreva uma expressão regular para reconhecer identificadores que devem obrigatoriamente começar com um sublinhado _, seguido por pelo menos uma letra maiúscula, podendo terminar com qualquer combinação de letras ou dígitos.

  3. Explique o motivo crucial de a instrução lexer->start = lexer->current; ocorrer após a chamada de skip_whitespace(lexer);. O que aconteceria se invertêssemos essas linhas?

  4. Explique detalhadamente como a função match resolve a ambiguidade léxica entre os operadores = (atribuição) e == (igualdade) ao encontrar os caracteres == no código.

Fim!

Até semana que vem (avaliação)!