Fenner
Compilador

Como o compilador funciona

O pipeline do fc, de um .fxp ao .exe: leitura, poda, geração de LLVM IR, ligação e os caminhos alternativos.

O fc é escrito em Fenner e compila a si mesmo: o compilador oficial sai de um bootstrap de várias gerações, e a paridade entre elas (a geração seguinte compilada pela anterior) é o teste de que ele se reproduz. Esta página descreve o caminho que um programa percorre dentro dele.

texto
fonte .fxp ──► leitura e análise ──► poda por vivos ──► geração de IR ──► LLVM ──► ligação ──► .exe
                    │                                                      │
                    └── meta modules (programa temporário em compilação)   └── cache da RTL

1. Partida e configuração#

O fc carrega, lê a linha de comando e junta o fc.config, o <projeto>.config e o <projeto>.config.local. Veja Configuração.

2. Leitura, léxico e análise#

É a maior etapa, em tempo e em código. O analisador léxico transforma texto em tokens, trata as diretivas condicionais ({$ifdef}, {$config}, SysConst) e expande os {$include}. O parser é também o analisador semântico: ele já resolve nomes (módulos, uses, namespaces), escolhe sobrecargas (em passadas: exata, frouxa, com conversão), infere e confere tipos, calcula constantes, aplica as conversões implícitas e monomorfiza os genéricos, reanalisando o texto da declaração com o argumento no lugar do parâmetro.

Quando há meta module, esta etapa compila e roda um programa à parte, lê o retrato do que foi compilado e devolve as edições, depois refaz o mundo. Veja Metaprogramação.

3. Poda por vivos#

A partir das raízes (o corpo principal, as initialization e a lista de exports), o compilador marca o que é alcançável: rotinas, tipos, classes, RTTI. Tudo que não foi marcado é descartado, o que permite que um programa pequeno use uma RTL enorme sem pagar por ela. É também o que dá sentido ao Declared(X) de um late module: X sobreviveu à poda, ou não.

4. Geração de código#

O gerador produz LLVM IR em texto: as declarações externas, o corpo de cada rotina, os metadados de classes (a VMT e o RTTI), os globais e os finalizadores, e, com -g, a informação de depuração (CodeView/PDB). Aqui se aplicam as autorizações de otimização que o programa deu (pure, discardable, devirtualization…).

5. LLVM#

O LLVM lê o IR, aplica a otimização do nível pedido (-O0 a -O3) e emite o código objeto. O fc leva o LLVM dentro dele (por isso o fc.exe tem dezenas de megabytes), e não há ferramenta externa a instalar.

6. Ligação#

Importações de DLLs do sistema, recursos (.res, manifesto, ícone) e o lld-link em processo produzem o executável. No fim, o compilador grava o .fxdeps e, no package, o .fxd e o .fxnames.

O cache da RTL#

A RTL é a mesma em toda compilação, e otimizá-la de novo a cada build seria o custo fixo do ciclo. Ela pode virar um objeto cacheado (em %LOCALAPPDATA%\fc\rtlcache), e a escolha segue o nível: no desenvolvimento (-O0/-O1) o cache ganha nos dois eixos, tempo e tamanho; na entrega (-O2/-O3) ele impediria a poda do otimizador, que precisa ver o programa inteiro, e por isso o programa vai inteiro. Uma alteração na RTL invalida o cache todo.

Para onde vai o tempo#

Medido num programa mínimo (Hello), compilando a RTL do compilador (7,8 MB de fonte) em uma máquina de desenvolvimento antiga:

Fase-O0-O3
ler e analisar a RTL≈ 6,6 s≈ 6,7 s
poda por vivos0,2 s0,2 s
gerar o IR0,15 s1,5 s
LLVM (ler, otimizar, emitir)≈ 1,4 s≈ 9 s
ligação0,2 s0,1 s
total≈ 10 s≈ 19 s

Duas conclusões desta tabela. No ciclo do dia a dia (-O0), cerca de 70% do tempo é ler a RTL: o System é analisado do fonte a cada vez, a perto de 1 MB/s. E num aplicativo real, que compila contra a BedRock (≈ 23 MB de fonte), o perfil muda: o LLVM passa a dominar no -O2 e a compilação leva minutos. Esse tempo é o alvo de um plano de otimização do próprio compilador.

Caminhos alternativos#

A partir do mesmo front-end existem fins diferentes:

  • Bytecode (.fxb): o IR é traduzido para um bytecode compacto, executado por uma máquina virtual (fxvm) que roda dentro do processo do Windows, com o mesmo layout de records, classes e vetores do compilador nativo. Tira o LLVM e o ligador do caminho do ciclo do dia a dia; o binário final continua saindo do LLVM. Está em desenvolvimento.
  • Só o IR (-S), e outros modos de emissão para depuração do compilador.
  • Package: o .fxd, o .fxnames e a .lib. Veja Packages e o .fxd.

Os módulos do compilador#

MóduloO que faz
MCLexerléxico e diretivas
MCParserdeclarações, corpos, resolução de nomes e tipos, genéricos
MCAsta árvore e os tipos
MCVivosa poda por vivos
MCCodeGena geração de LLVM IR
MCLLVMa ponte para o LLVM
MCFxdo formato .fxd e o mapa de nomes
MCRttio RTTI externo (.rtti.json)
MCConfiga configuração
MCDepso .fxdeps