A construção de um compilador envolve etapas fundamentais de transformação de representações. Após a estruturação da Árvore de Sintaxe Abstrata (AST), o próximo passo consiste na geração de uma Representação Intermediária (IR) — neste caso, a Koopa IR — e sua subsequente tradução para linguagem de montagem (Assembly) RISC-V.
Geração de Koopa IR via Travessia de AST
Para gerar a Koopa IR, percorremos a AST de forma recursiva, convertendo cada nó em sua respectiva representação textual. O uso da classe std::ostringstream em C++ facilita a concatenação de strings complexas durante esse processo.
Abaixo, um exemplo de implementação de uma classe para definição de funções, demonstrando como a estrutura da AST pode ser mapeada para o formato da Koopa IR:
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
const char* TAB = " ";
class NodeBase {
public:
virtual ~NodeBase() = default;
virtual void emitIR(std::ostream &stream, std::string indent) const = 0;
};
class FuncDeclNode : public NodeBase {
public:
std::unique_ptr<NodeBase> return_type;
std::string name;
std::unique_ptr<NodeBase> body;
void emitIR(std::ostream &stream, std::string indent) const override {
stream << indent << "fun @" << name << "(): ";
return_type->emitIR(stream, "");
stream << " {\n";
body->emitIR(stream, indent + TAB);
stream << "\n" << indent << "}";
}
};
Ao utilizar std::ostringstream, é importante atentar para o fato de que o método .str() retorna um objeto temporário. Para processar o conteúdo com a biblioteca libkoopa (usando koopa_parse_from_string), é necessário armazenar esse resultado em uma variável std::string persistnete para garantir que o ponteiro de caracteres permaneça válido durante o parsing.
Tradução de Koopa IR para RISC-V
Uma vez obtida a representação em Koopa IR, o compilador deve traduzir esses comandos para instruções de máquina. O processo de conversão para RISC-V segue uma lógica similar à geração da IR: percorre-se a estrutura de dados da koopa_raw_program_t (gerada pelo parser da biblioteca) e emite-se o código assembly correspondente para cada instrução, gerenciando registradores e pilha.
Gerenciamento de Argumentos via Linha de Comando
Para que o compilador seja funcional, ele deve aceitar parâmetros que definam o modo de operação (gerar IR ou gerar Assembly) e o caminho do arquivo de saída. A função getopt_long_only da biblioteca padrão C é ideal para tratar argumentos que utilizam apenas um hífen, como exigido em certas especificações acadêmicas.
#include <getopt.h>
#include <string>
#include <cassert>
struct Config {
bool emit_ir_only = false;
std::string output_file;
};
void parse_cli(int argc, char **argv, Config &config) {
static struct option options[] = {
{"koopa", no_argument, nullptr, 'k'},
{"riscv", no_argument, nullptr, 'r'},
{"output", required_argument, nullptr, 'o'},
{nullptr, 0, nullptr, 0 }
};
int opt;
while ((opt = getopt_long_only(argc, argv, "ko:", options, nullptr)) != -1) {
switch (opt) {
case 'k':
config.emit_ir_only = true;
break;
case 'r':
config.emit_ir_only = false;
break;
case 'o':
config.output_file = optarg;
break;
case '?':
exit(1);
default:
assert(false && "Opção não implementada");
}
}
}
Esta abordagem permite que o binário suporte chamadas como compiler -koopa input.sysy -o output.koopa, garantindo flexibilidade no fluxo de compilação.