Introdução15.1
No laboratório anterior de implementação, finalizamos o analisador sintático estrutural do Slang. Até esse ponto, o compilador já consegue executar duas etapas importantes:
- transformar o código-fonte em tokens com o modo
lex; - transformar o código-fonte em uma AST completa com o modo
parse.
Isso significa que o front-end do compilador já está funcionando. O programa abaixo, por exemplo:
fn main(args: void) -> void {
value: int = 5
print(value)
}
já pode ser lido, tokenizado, analisado sintaticamente e impresso como árvore. Porém, ele ainda não pode ser executado, porque falta uma etapa essencial: transformar a AST em instruções que alguma máquina consiga executar.
Neste laboratório, vamos iniciar a fase de geração de código. O objetivo será percorrer a AST produzida pelo parser e gerar assembly textual para uma máquina virtual de pilha, a SlangVM.
O fluxo que estamos construindo é:
Na prática, queremos chegar a este fluxo de comandos:
./build/compiler code programa.slang > programa.slasm
./build/slang-vm programa.slasm
Neste primeiro laboratório de geração de código, vamos preparar a infraestrutura e gerar código para uma parte controlada da linguagem:
- literais;
- variáveis locais;
- expressões aritméticas;
- expressões de comparação;
- chamadas de função;
print;- funções simples com
return.
Vamos deixar if, loop, break, continue e curto-circuito lógico para a segunda parte. Esses recursos exigem saltos e labels; por isso, fazem mais sentido depois que a base da VM e do gerador já estiver funcionando.
O que muda no compilador15.2
No fim do laboratório 12, o executável aceitava dois modos:
./build/compiler lex arquivo.slang
./build/compiler parse arquivo.slang
Agora vamos adicionar um terceiro modo:
./build/compiler code arquivo.slang
Esse novo modo fará quatro coisas:
- inicializar o parser;
- construir a AST do programa;
- percorrer a AST com o gerador de código;
- escrever assembly da SlangVM em
stdout.
Também vamos adicionar um segundo executável ao projeto:
./build/slang-vm arquivo.slasm
O compilador gera o arquivo .slasm; a VM executa esse arquivo. Essa separação é didática e prática: conseguimos inspecionar o assembly gerado antes de executar o programa.
A ideia da SlangVM15.3
A SlangVM é uma máquina de pilha. Isso significa que suas instruções manipulam valores no topo de uma pilha, em vez de usar registradores explícitos.
Por exemplo, a expressão:
1 + 2 * 3
será traduzida para:
push 1
push 2
push 3
mul
add
A execução ocorre assim:
push 1empilha1;push 2empilha2;push 3empilha3;mulremove2e3, calcula6e empilha o resultado;addremove1e6, calcula7e empilha o resultado.
Essa estratégia simplifica muito a geração de código. Em vez de escolher registradores para cada valor temporário, o compilador apenas emite instruções que empilham operandos e consomem esses operandos na ordem correta.
Instruções da SlangVM que usaremos15.3.1
O codegin vai emitir instruções de três categorias:
Manipulação da pilha:
| Instrução | Efeito |
|---|---|
push N |
Empilha o valor N |
pop |
Remove o valor do topo |
dup |
Duplica o valor do topo |
Variáveis locais:
| Instrução | Efeito |
|---|---|
get_local N |
Empilha o valor do slot local N |
set_local N |
Desempilha e armazena no slot N |
Aritmética e comparação:
| Instrução | Operação |
|---|---|
add, sub, mul, div, mod |
Desempilha dois, empilha resultado |
eq, ne, lt, le, gt, ge |
Desempilha dois, empilha true/false |
neg, not |
Desempilha um, empilha resultado |
Controle e funções:
| Instrução | Efeito |
|---|---|
call N fn_label |
Chama função com N argumentos |
ret |
Retorna da função |
halt |
Encerra o programa |
print |
Imprime o valor do topo |
jump L, jump_if_false L |
Saltos (usados na Parte II) |
Não é preciso decorar essa tabela agora. Durante a implementação, cada instrução será apresentada no contexto em que aparece.
Preparando o alvo de execução15.4
Antes de gerar código, precisamos ter uma máquina capaz de executar esse código. Por isso, o primeiro conjunto de mudanças adiciona a SlangVM ao projeto e define a interface entre o compilador e o gerador.
Adicionando a VM ao projeto15.4.1
Crie as pastas:
include/vm/
src/vm/
Dentro delas, adicione os arquivos da VM fornecidos com o material da aula:
include/vm/value.h
include/vm/chunk.h
include/vm/vm.h
include/vm/assembler.h
include/vm/debug.h
src/vm/chunk.c
src/vm/vm.c
src/vm/assembler.c
src/vm/debug.c
src/vm/main.c
O papel desses arquivos é:
value.h: define os tipos de valores que a VM manipula;chunk.hechunk.c: armazenam bytecode e constantes;vm.hevm.c: executam as instruções;assembler.heassembler.c: transformam assembly textual em bytecode;debug.hedebug.c: ajudam a imprimir bytecode durante depuração;src/vm/main.c: cria o executávelslang-vm.
Neste momento, o compilador ainda não gera código para a VM. Estamos apenas preparando o alvo de execução.
Os arquivos fornecidos contêm dois bugs que precisam ser corrigidos para que programas com recursão, if, loop e labels funcionem corretamente:
1. src/vm/vm.c — return_slot em OP_RET
O OP_RET original lê o return_slot do quadro chamador (caller->return_slot), mas deveria ler do quadro sendo destruído (cur->return_slot). Sem essa correção, chamadas de função aninhadas (como recursão) retornam null em vez do valor correto.
Substitua o trecho:
case OP_RET: {
Value result = vm->stack_top == 0 ? value_null() : vm_pop(vm);
vm->frame_count--;
if (vm->frame_count > 0) {
CallFrame *caller = &vm->frames[vm->frame_count - 1];
if (caller->return_slot >= 0) {
vm->stack_top = caller->return_slot;
vm_push(vm, result);
}
}
break;
}
por:
case OP_RET: {
Value result = vm->stack_top == 0 ? value_null() : vm_pop(vm);
int return_slot = cur->return_slot;
vm->frame_count--;
if (vm->frame_count > 0 && return_slot >= 0) {
vm->stack_top = return_slot;
vm_push(vm, result);
}
break;
}
2. src/vm/assembler.c — sistema de labels separado das constantes do chunk
O find_or_create_label original armazena labels como constantes string no chunk, mas depois as busca iterando sobre constants[i].as.string. Quando uma constante numérica aparece antes de uma label (ex.: push 5 antes de jump_if_false L0), o acesso a as.string de um número causa segfault.
A correção envolve três mudanças:
a) Em include/vm/assembler.h, substitua o campo int *label_addresses por um vetor de structs Label:
typedef struct {
char *name;
int name_len;
int address;
} Label;
typedef struct {
Chunk chunk;
Label *labels;
int label_count;
int label_capacity;
int *fixup_ips;
int *fixup_labels;
int fixup_count;
int fixup_capacity;
} Assembler;
b) Em src/vm/assembler.c, reescreva assembler_init e find_or_create_label:
static int find_label(Assembler *assembler, const char *name, int name_len) {
for (int i = 0; i < assembler->label_count; i++) {
if (assembler->labels[i].name_len == name_len &&
memcmp(assembler->labels[i].name, name, name_len) == 0) {
return i;
}
}
return -1;
}
static int create_label(Assembler *assembler, const char *name, int name_len) {
if (assembler->label_capacity <= assembler->label_count) {
int old = assembler->label_capacity;
assembler->label_capacity = old < 8 ? 8 : old * 2;
assembler->labels = (Label *)realloc(assembler->labels,
sizeof(Label) * assembler->label_capacity);
}
int idx = assembler->label_count++;
assembler->labels[idx].name = (char *)malloc(name_len + 1);
memcpy(assembler->labels[idx].name, name, name_len);
assembler->labels[idx].name[name_len] = '\0';
assembler->labels[idx].name_len = name_len;
assembler->labels[idx].address = -1;
return idx;
}
void assembler_init(Assembler *assembler) {
chunk_init(&assembler->chunk);
assembler->labels = NULL;
assembler->label_count = 0;
assembler->label_capacity = 0;
assembler->fixup_ips = NULL;
assembler->fixup_labels = NULL;
assembler->fixup_count = 0;
assembler->fixup_capacity = 0;
}
static int find_or_create_label(Assembler *assembler, const char *name, int name_len) {
int idx = find_label(assembler, name, name_len);
if (idx >= 0) return idx;
return create_label(assembler, name, name_len);
}
c) Em src/vm/assembler.c, em todas as ocorrências de assembler->label_addresses[...] = ..., substitua por assembler->labels[...].address = .... Em resolve_fixups, use assembler->labels[label_idx].address. Em assembler_free, libere cada labels[i].name antes de liberar o vetor.
Essas correções são necessárias para que a VM funcione com todos os recursos que implementaremos no próximo laboratório.
Criando a interface do gerador15.4.2
Crie o arquivo include/compiler/codegen.h:
// include/compiler/codegen.h
#ifndef COMPILER_CODEGEN_H_
#define COMPILER_CODEGEN_H_
#include <stdbool.h>
#include <stdio.h>
#include "compiler/ast.h"
bool codegen_generate(FILE *stream, const AstNode *program);
#endif
A função codegen_generate recebe dois argumentos:
stream, que indica onde o assembly será escrito;program, que é a raiz da AST gerada pelo parser.
Usar um FILE * deixa o gerador flexível. Nos testes e no modo code, vamos passar stdout; no futuro, poderíamos passar um arquivo aberto ou até um buffer de memória.
Estado interno do gerador15.5
Com a VM disponível, precisamos representar o estado usado durante a geração. Esse estado concentra a saída do assembly, a tabela de variáveis locais, os contadores de labels e as informações que serão usadas mais tarde em laços.
Criando a estrutura Codegen15.5.1
Crie src/compiler/codegen.c e comece com a estrutura abaixo:
// src/compiler/codegen.c
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "compiler/codegen.h"
#define MAX_SYMBOLS 256
#define MAX_LOOPS 64
typedef struct {
const char *start;
int length;
int slot;
} Symbol;
typedef struct {
int start_label;
int end_label;
} LoopLabels;
typedef struct {
FILE *stream;
Symbol symbols[MAX_SYMBOLS];
int symbol_count;
int next_slot;
LoopLabels loops[MAX_LOOPS];
int loop_count;
int next_label;
bool had_error;
} Codegen;
static void gen_declaration(Codegen *codegen, const AstNode *node);
static void gen_statement(Codegen *codegen, const AstNode *node);
static void gen_expression(Codegen *codegen, const AstNode *node);
A estrutura Codegen guarda tudo que o gerador precisa enquanto percorre a AST:
streamé o destino do assembly;symbolsassocia nomes de variáveis a slots locais;next_slotindica qual será o próximo slot livre;loopsserá usado na segunda parte parabreakecontinue;next_labelserá usado para criar labels únicas;had_errorinterrompe a geração quando encontramos algo inválido.
Mesmo que LoopLabels só seja usado na segunda parte, já deixamos a estrutura preparada. Assim, a forma geral do gerador não muda quando adicionarmos controle de fluxo.
Trabalhando com tokens e símbolos15.5.2
O gerador precisa comparar tokens com nomes especiais, como main e print. Como os tokens apontam para fatias do código-fonte original, não podemos usar strcmp diretamente.
Adicione:
static bool token_equals(Token token, const char *text) {
int length = (int)strlen(text);
return token.length == length && memcmp(token.start, text, length) == 0;
}
static void print_token(FILE *stream, Token token) {
fprintf(stream, "%.*s", token.length, token.start);
}
static int new_label(Codegen *codegen) {
return codegen->next_label++;
}
Agora precisamos de uma tabela simples para variáveis locais. Cada variável será associada a um slot numérico da VM.
Adicione:
static int find_symbol(Codegen *codegen, Token name) {
for (int i = codegen->symbol_count - 1; i >= 0; i--) {
Symbol symbol = codegen->symbols[i];
if (symbol.length == name.length && memcmp(symbol.start, name.start, name.length) == 0) {
return symbol.slot;
}
}
fprintf(stderr, "Erro de geração: variável '%.*s' não declarada.\n", name.length, name.start);
codegen->had_error = true;
return 0;
}
static int declare_symbol(Codegen *codegen, Token name) {
if (codegen->symbol_count == MAX_SYMBOLS) {
fprintf(stderr, "Erro de geração: muitas variáveis locais.\n");
codegen->had_error = true;
return 0;
}
int slot = codegen->next_slot++;
codegen->symbols[codegen->symbol_count++] = (Symbol){name.start, name.length, slot};
return slot;
}
Essa tabela ainda não é uma análise semântica completa. Ela apenas responde: “qual slot local representa este nome?”.
Emitindo literais e nomes de função15.5.3
Strings precisam ser emitidas entre aspas no assembly. Também precisamos preservar escapes básicos para não quebrar o formato textual.
Adicione:
static void emit_string_literal(FILE *stream, Token token) {
fputc('"', stream);
for (int i = 1; i < token.length - 1; i++) {
char c = token.start[i];
if (c == '"' || c == '\\') {
fputc('\\', stream);
}
fputc(c, stream);
}
fputc('"', stream);
}
Também vamos padronizar labels de função. A função Slang main vira fn_main; a função add vira fn_add.
static void emit_function_label(FILE *stream, Token name) {
fputs("fn_", stream);
print_token(stream, name);
}
Esse prefixo evita colisão com labels internas como L0, L1 e __entry.
Gerando os primeiros elementos da linguagem15.6
Agora que o gerador já tem estado interno e funções auxiliares, podemos começar a percorrer a AST. Nesta parte, vamos gerar funções, variáveis, chamadas e expressões simples.
Encontrando a função main15.6.1
O arquivo gerado precisa começar chamando uma função inicial. Em programas normais, essa função será main.
Adicione:
static const AstNode *find_main(const AstNode *program) {
for (int i = 0; i < program->children.count; i++) {
const AstNode *child = program->children.items[i];
if (child->kind == AST_FUNCTION && token_equals(child->token, "main")) {
return child;
}
}
return NULL;
}
static bool program_has_top_level_code(const AstNode *program) {
for (int i = 0; i < program->children.count; i++) {
if (program->children.items[i]->kind != AST_FUNCTION) {
return true;
}
}
return false;
}
Se existir main, o assembly começa chamando fn_main. Se não existir, criaremos uma entrada sintética chamada __entry para executar declarações de topo.
Gerando blocos, funções e variáveis15.6.2
Um bloco é uma sequência de declarações. Portanto, a geração de bloco apenas percorre seus filhos:
static void gen_block(Codegen *codegen, const AstNode *block) {
for (int i = 0; i < block->children.count; i++) {
gen_declaration(codegen, block->children.items[i]);
}
}
Uma função vira uma label. Seus parâmetros ocupam os primeiros slots locais:
static void gen_function(Codegen *codegen, const AstNode *function) {
fputs("label ", codegen->stream);
emit_function_label(codegen->stream, function->token);
fputc('\n', codegen->stream);
codegen->symbol_count = 0;
codegen->next_slot = 0;
codegen->loop_count = 0;
for (int i = 0; i < function->children.count; i++) {
declare_symbol(codegen, function->children.items[i]->token);
}
gen_block(codegen, function->first);
fputs("push null\nret\n", codegen->stream);
}
A lógica é a seguinte:
- antes de gerar uma função, limpamos a tabela de símbolos;
- cada parâmetro recebe um slot local;
- o corpo da função é gerado normalmente;
- se a função terminar sem
return, emitimospush nulleretcomo retorno padrão.
Uma declaração de variável gera o inicializador, salva o resultado no slot novo e remove o valor da pilha:
static void gen_var_decl(Codegen *codegen, const AstNode *node) {
int slot = declare_symbol(codegen, node->token);
gen_expression(codegen, node->first);
fprintf(codegen->stream, "set_local %d\npop\n", slot);
}
O inicializador deixa um valor no topo da pilha. A instrução set_local copia esse valor para o slot da variável, e o pop logo em seguida remove o valor original da pilha — evitando acúmulo de lixo a cada declaração.
Gerando chamadas de função15.6.3
Chamadas comuns avaliam os argumentos da esquerda para a direita e emitem call:
static void gen_call(Codegen *codegen, const AstNode *node) {
if (node->first != NULL && node->first->kind == AST_IDENTIFIER && token_equals(node->first->token, "print")) {
for (int i = 0; i < node->children.count; i++) {
gen_expression(codegen, node->children.items[i]);
fputs("print\n", codegen->stream);
}
fputs("push null\n", codegen->stream);
return;
}
for (int i = 0; i < node->children.count; i++) {
gen_expression(codegen, node->children.items[i]);
}
if (node->first == NULL || node->first->kind != AST_IDENTIFIER) {
fprintf(stderr, "Erro de geração: chamada indireta não suportada.\n");
codegen->had_error = true;
return;
}
fprintf(codegen->stream, "call %d ", node->children.count);
emit_function_label(codegen->stream, node->first->token);
fputc('\n', codegen->stream);
}
Aqui fazemos um tratamento especial para print. Em vez de exigir uma função Slang chamada print, geramos diretamente a instrução print da VM. Depois disso, empilhamos null para manter a chamada como uma expressão que produz algum valor.
Gerando expressões simples15.6.4
Operadores binários simples seguem sempre o mesmo padrão:
- gerar o lado esquerdo;
- gerar o lado direito;
- emitir a instrução que consome os dois valores.
Adicione:
static void gen_binary(Codegen *codegen, const AstNode *node) {
gen_expression(codegen, node->first);
gen_expression(codegen, node->second);
switch (node->token.type) {
case TOK_PLUS: fputs("add\n", codegen->stream); break;
case TOK_MINUS: fputs("sub\n", codegen->stream); break;
case TOK_STAR: fputs("mul\n", codegen->stream); break;
case TOK_SLASH: fputs("div\n", codegen->stream); break;
case TOK_PERCENT: fputs("mod\n", codegen->stream); break;
case TOK_EQ: fputs("eq\n", codegen->stream); break;
case TOK_BANG_EQ: fputs("ne\n", codegen->stream); break;
case TOK_LT: fputs("lt\n", codegen->stream); break;
case TOK_LT_EQ: fputs("le\n", codegen->stream); break;
case TOK_GT: fputs("gt\n", codegen->stream); break;
case TOK_GT_EQ: fputs("ge\n", codegen->stream); break;
default:
fprintf(stderr, "Erro de geração: operador binário não suportado.\n");
codegen->had_error = true;
break;
}
}
Neste primeiro momento, and e or ainda não entram aqui. Eles exigem curto-circuito com saltos, então ficarão para a segunda parte.
Agora adicione a função principal de geração de expressões:
static void gen_expression(Codegen *codegen, const AstNode *node) {
if (node == NULL || codegen->had_error) {
return;
}
switch (node->kind) {
case AST_BINARY:
gen_binary(codegen, node);
break;
case AST_UNARY:
gen_expression(codegen, node->first);
fputs(node->token.type == TOK_MINUS ? "neg\n" : "not\n", codegen->stream);
break;
case AST_CALL:
gen_call(codegen, node);
break;
case AST_IDENTIFIER:
fprintf(codegen->stream, "get_local %d\n", find_symbol(codegen, node->token));
break;
case AST_NUMBER:
fputs("push ", codegen->stream);
print_token(codegen->stream, node->token);
fputc('\n', codegen->stream);
break;
case AST_STRING:
fputs("push ", codegen->stream);
emit_string_literal(codegen->stream, node->token);
fputc('\n', codegen->stream);
break;
case AST_CHAR:
fputs("push ", codegen->stream);
print_token(codegen->stream, node->token);
fputc('\n', codegen->stream);
break;
case AST_BOOL:
fputs(node->token.type == TOK_KW_TRUE ? "push true\n" : "push false\n", codegen->stream);
break;
default:
fprintf(stderr, "Erro de geração: expressão não suportada.\n");
codegen->had_error = true;
break;
}
}
Cada caso da AST deixa exatamente um valor no topo da pilha. Essa regra é importante: se toda expressão deixa um valor, os comandos podem decidir se usam esse valor, armazenam esse valor ou descartam esse valor.
Simulação manual: percorrendo uma expressão no codegen15.6.5
Para visualizar como essas funções se encaixam, vamos simular a geração de:
print(1 + 2 * 3)
O codegen começa em gen_expression com o nó AST_CALL que representa print(...). Como print é uma função especial, gen_call é chamada e faz o seguinte:
- identifica que o alvo é
print; - para cada argumento (no caso, um só), chama
gen_expression; - dentro dessa chamada, o argumento é um nó
AST_BINARYcom operador+; gen_expressiondespacha paragen_binary, que por sua vez:- chama
gen_expressionno lado esquerdo (1): emitepush 1; - chama
gen_expressionno lado direito (2 * 3): é outroAST_BINARYcom*; gen_binarypara*emitegen_expression(2)→push 2,gen_expression(3)→push 3, e depoismul;
- chama
- voltando ao
+, emiteadd; - volta para
gen_call, que emiteprinte depoispush null.
O assembly gerado fica:
push 1
push 2
push 3
mul
add
print
push null
Esse push null final é o valor de retorno de print — como print é uma expressão (toda chamada é), ela precisa deixar um valor na pilha. O null indica "nenhum valor relevante". Quem chamou print decide se usa ou descarta esse valor.
Gerando comandos simples15.6.6
Nesta primeira parte, vamos gerar apenas comandos que não exigem saltos.
Adicione gen_statement com os casos abaixo:
static void gen_statement(Codegen *codegen, const AstNode *node) {
if (node == NULL || codegen->had_error) {
return;
}
switch (node->kind) {
case AST_BLOCK:
gen_block(codegen, node);
break;
case AST_VAR_DECL:
gen_var_decl(codegen, node);
break;
case AST_RETURN:
if (node->first != NULL) {
gen_expression(codegen, node->first);
} else {
fputs("push null\n", codegen->stream);
}
fputs("ret\n", codegen->stream);
break;
case AST_ASSIGN:
gen_expression(codegen, node->first);
fprintf(codegen->stream, "set_local %d\npop\n", find_symbol(codegen, node->token));
break;
case AST_EXPR_STMT:
gen_expression(codegen, node->first);
fputs("pop\n", codegen->stream);
break;
default:
fprintf(stderr, "Erro de geração: comando ainda não suportado nesta etapa.\n");
codegen->had_error = true;
break;
}
}
Observe a diferença entre expressão e comando:
- uma expressão deixa valor na pilha;
- um comando normalmente não deve deixar lixo na pilha;
- por isso
AST_EXPR_STMTgera a expressão e depois emitepop.
Ainda não tratamos AST_IF, AST_LOOP, AST_BREAK e AST_CONTINUE. Isso fica para a segunda parte, quando vamos falar de labels e saltos.
Gerando a entrada do programa15.6.7
Agora precisamos ligar as funções anteriores ao programa inteiro.
Adicione as funções finais do arquivo:
static void gen_declaration(Codegen *codegen, const AstNode *node) {
if (node->kind != AST_FUNCTION) {
gen_statement(codegen, node);
}
}
static void gen_entry(Codegen *codegen, const AstNode *program) {
fputs("label __entry\n", codegen->stream);
codegen->symbol_count = 0;
codegen->next_slot = 0;
codegen->loop_count = 0;
for (int i = 0; i < program->children.count; i++) {
const AstNode *child = program->children.items[i];
if (child->kind != AST_FUNCTION) {
gen_declaration(codegen, child);
}
}
fputs("push null\nret\n", codegen->stream);
}
bool codegen_generate(FILE *stream, const AstNode *program) {
Codegen codegen = {0};
codegen.stream = stream;
const AstNode *main_function = find_main(program);
bool has_main = main_function != NULL;
bool has_top_level = program_has_top_level_code(program);
if (has_main) {
for (int i = 0; i < main_function->children.count; i++) {
fputs("push null\n", stream);
}
fprintf(stream, "call %d fn_main\nhalt\n", main_function->children.count);
} else {
fputs("call 0 __entry\nhalt\n", stream);
}
for (int i = 0; i < program->children.count; i++) {
const AstNode *child = program->children.items[i];
if (child->kind == AST_FUNCTION) {
gen_function(&codegen, child);
}
}
if (has_top_level || !has_main) {
gen_entry(&codegen, program);
}
return !codegen.had_error;
}
Essa versão já é suficiente para programas sem controle de fluxo. A versão mais completa, com if, loop, break, continue e curto-circuito lógico, será consolidada no próximo laboratório.
Integrando o gerador ao compilador15.7
Depois de implementar o gerador, precisamos expô-lo na linha de comando e garantir que o sistema de build compile todos os novos arquivos.
Atualizando o main.c15.7.1
Precisamos expor o novo modo code na interface do compilador.
No topo de src/main.c, adicione:
#include <stdbool.h>
#include "compiler/codegen.h"
Depois de run_parser_mode, adicione:
static int run_codegen_mode(const char *source) {
Parser parser;
parser_init(&parser, source, stderr);
AstNode *program = parser_parse(&parser);
if (program == NULL) {
return 65;
}
bool ok = codegen_generate(stdout, program);
ast_free(program);
return ok ? 0 : 65;
}
Agora ajuste a mensagem de uso:
fprintf(stderr, "Uso: compiler <lex|parse|code> <arquivo>\n");
E adicione o novo ramo no main:
} else if (strcmp(argv[1], "code") == 0) {
result = run_codegen_mode(source);
Atualizando o CMakeLists.txt15.7.2
Primeiro, adicione src/compiler/codegen.c na biblioteca:
add_library(compiler_lib
src/compiler/token.c
src/compiler/lexer.c
src/compiler/ast.c
src/compiler/parser.c
src/compiler/codegen.c
)
Depois, adicione o executável da VM:
add_executable(slang-vm
src/vm/main.c
src/vm/assembler.c
src/vm/chunk.c
src/vm/debug.c
src/vm/vm.c
)
target_include_directories(slang-vm
PRIVATE
${PROJECT_SOURCE_DIR}/include
)
target_compile_options(slang-vm PRIVATE
-Wall -Wextra -Wpedantic
)
Agora o projeto deve gerar dois executáveis de uso direto:
build/compiler;build/slang-vm.
Testando a primeira versão do gerador15.8
Com a integração pronta, já podemos gerar assembly e executar programas simples na SlangVM.
Primeiro teste manual15.8.1
Crie um arquivo chamado programa.slang:
fn main(args: void) -> void {
print(1 + 2 * 3)
}
Compile o projeto:
cmake -B build
cmake --build build
Gere assembly:
./build/compiler code programa.slang
A saída deve ser parecida com:
push null
call 1 fn_main
halt
label fn_main
push 1
push 2
push 3
mul
add
print
push null
pop
push null
ret
push null e pop extrasO assembly gerado tem algumas instruções que podem parecer repetidas à primeira vista. Cada uma tem uma origem diferente:
push nullantes decall: valor passado como argumento para o parâmetroargsdemain.print: consome um valor (o resultado de1 + 2 * 3) e o imprime.push nulldepois deprint: é o valor de retorno deprint(toda chamada deixa algo na pilha).poplogo em seguida: o comandoprint(...)é uma expressão usada como comando, então o gerador descarta o valor deixado pela chamada.push nullantes deret: retorno padrão que o gerador insere quando a função termina sem umreturnexplícito.
Na prática, apenas push 1, push 2, push 3, mul, add e print são essenciais para o cálculo. As demais instruções são necessárias por causa da convenção de pilha que adotamos.
Agora salve e execute:
./build/compiler code programa.slang > programa.slasm
./build/slang-vm programa.slasm
A saída esperada é:
7
Testando chamada de função15.8.2
Agora troque programa.slang por:
fn add(a: int, b: int) -> int {
return a + b
}
fn main(args: void) -> void {
x: int = 10
print(add(x, 32))
}
Gere e execute:
./build/compiler code programa.slang > programa.slasm
./build/slang-vm programa.slasm
A saída esperada é:
42
Esse teste valida:
- parâmetros de função;
- variável local;
- leitura com
get_local; - escrita com
set_local; - chamada com
call; - retorno com
ret.
Próximos passos15.9
Neste ponto, já temos a primeira versão da geração de código. Ela consegue executar programas com funções, variáveis, expressões e chamadas simples.
No próximo laboratório, Lab - Geração de Código II, vamos completar o compilador adicionando:
ifeelse;loop;breakecontinue;- curto-circuito de
andeor; - testes completos de VM, assembly e opcodes.
Também será no próximo laboratório que vamos consolidar uma versão mais completa do gerador, reunindo os casos simples desta parte com os casos de controle de fluxo.