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.
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 RTL1. 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 vivos | 0,2 s | 0,2 s |
| gerar o IR | 0,15 s | 1,5 s |
| LLVM (ler, otimizar, emitir) | ≈ 1,4 s | ≈ 9 s |
| ligação | 0,2 s | 0,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.fxnamese a.lib. Veja Packages e o .fxd.
Os módulos do compilador#
| Módulo | O que faz |
|---|---|
MCLexer | léxico e diretivas |
MCParser | declarações, corpos, resolução de nomes e tipos, genéricos |
MCAst | a árvore e os tipos |
MCVivos | a poda por vivos |
MCCodeGen | a geração de LLVM IR |
MCLLVM | a ponte para o LLVM |
MCFxd | o formato .fxd e o mapa de nomes |
MCRtti | o RTTI externo (.rtti.json) |
MCConfig | a configuração |
MCDeps | o .fxdeps |