jak-project/goalc/compiler/Compiler.cpp
Tyler Wilding 10ac78200b
repl: add gameVersionFolder to repl-config for running the non-default version (#2463)
Adds a decent way to customize the folders the project file expects the
iso data and decompiler data to be in. When you run any version other
than the default, for example Jak 1 PAL, it uses the `gameName`
decompiler config to consume and output it's results.

However the project file will assume `jak1` unless you hard-code it
differently -- basically, it needs to be explicitly told just the
decompiler is told what version to use.

We now have a per-user REPL Config json file, so that can be used to
override the default `jak1` behaviour.

Fixes #1993
2023-04-11 17:57:20 -04:00

516 lines
17 KiB
C++

#include "Compiler.h"
#include <chrono>
#include <thread>
#include "CompilerException.h"
#include "IR.h"
#include "common/goos/PrettyPrinter.h"
#include "common/link_types.h"
#include "common/util/FileUtil.h"
#include "goalc/make/Tools.h"
#include "goalc/regalloc/Allocator.h"
#include "goalc/regalloc/Allocator_v2.h"
#include "third-party/fmt/core.h"
using namespace goos;
Compiler::Compiler(GameVersion version,
const std::optional<REPL::Config> repl_config,
const std::string& user_profile,
std::unique_ptr<REPL::Wrapper> repl)
: m_version(version),
m_goos(user_profile),
m_debugger(&m_listener, &m_goos.reader, version),
m_repl(std::move(repl)),
m_make(repl_config, user_profile) {
m_listener.add_debugger(&m_debugger);
m_listener.set_default_port(version);
m_ts.add_builtin_types(m_version);
m_global_env = std::make_unique<GlobalEnv>();
m_none = std::make_unique<None>(m_ts.make_typespec("none"));
// let the build system run us
m_make.add_tool(std::make_shared<CompilerTool>(this));
// define game version before loading goal-lib.gc
m_goos.set_global_variable_by_name("GAME_VERSION", m_goos.intern(game_version_names[m_version]));
// load GOAL library
Object library_code = m_goos.reader.read_from_file({"goal_src", "goal-lib.gc"});
compile_object_file("goal-lib", library_code, false);
// user profile stuff
if (user_profile != "#f" && fs::exists(file_util::get_jak_project_dir() / "goal_src" / "user" /
user_profile / "user.gc")) {
try {
Object user_code =
m_goos.reader.read_from_file({"goal_src", "user", user_profile, "user.gc"});
compile_object_file(user_profile, user_code, false);
} catch (std::exception& e) {
print_compiler_warning("REPL Warning: {}\n", e.what());
}
}
// add built-in forms to symbol info
for (const auto& [builtin_name, builtin_info] : g_goal_forms) {
SymbolInfo::Metadata sym_meta;
sym_meta.docstring = builtin_info.first;
m_symbol_info.add_builtin(builtin_name, sym_meta);
}
// load auto-complete history, only if we are running in the interactive mode.
if (m_repl) {
m_repl->load_history();
// init repl
m_repl->print_welcome_message();
auto& examples = m_repl->examples;
auto& regex_colors = m_repl->regex_colors;
m_repl->init_settings();
using namespace std::placeholders;
m_repl->get_repl().set_completion_callback(std::bind(
&Compiler::find_symbols_or_object_file_by_prefix, this, _1, _2, std::cref(examples)));
m_repl->get_repl().set_hint_callback(
std::bind(&Compiler::find_hints_by_prefix, this, _1, _2, _3, std::cref(examples)));
m_repl->get_repl().set_highlighter_callback(
std::bind(&Compiler::repl_coloring, this, _1, _2, std::cref(regex_colors)));
}
// add GOOS forms that get info from the compiler
setup_goos_forms();
}
Compiler::~Compiler() {
if (m_listener.is_connected()) {
m_listener.send_reset(false); // reset the target
m_listener.disconnect();
}
}
ReplStatus Compiler::handle_repl_string(const std::string& input) {
if (input.empty()) {
return ReplStatus::OK;
}
try {
// 1). read
goos::Object code = m_goos.reader.read_from_string(input, true);
// 2). compile
auto obj_file = compile_object_file("repl", code, m_listener.is_connected());
if (m_settings.debug_print_ir) {
obj_file->debug_print_tl();
}
if (!obj_file->is_empty()) {
// 3). color
color_object_file(obj_file);
// 4). codegen
auto data = codegen_object_file(obj_file);
// 4). send!
if (m_listener.is_connected()) {
m_listener.send_code(data);
if (!m_listener.most_recent_send_was_acked()) {
print_compiler_warning("Runtime is not responding. Did it crash?\n");
}
}
}
} catch (std::exception& e) {
print_compiler_warning("REPL Error: {}\n", e.what());
}
if (m_want_exit) {
return ReplStatus::WANT_EXIT;
}
if (m_want_reload) {
return ReplStatus::WANT_RELOAD;
}
return ReplStatus::OK;
}
FileEnv* Compiler::compile_object_file(const std::string& name,
goos::Object code,
bool allow_emit) {
auto file_env = m_global_env->add_file(name);
Env* compilation_env = file_env;
file_env->add_top_level_function(
compile_top_level_function("top-level", std::move(code), compilation_env));
if (!allow_emit && !file_env->is_empty()) {
throw std::runtime_error("Compilation generated code, but wasn't supposed to");
}
return file_env;
}
std::unique_ptr<FunctionEnv> Compiler::compile_top_level_function(const std::string& name,
const goos::Object& code,
Env* env) {
auto fe = std::make_unique<FunctionEnv>(env, name, &m_goos.reader);
fe->set_segment(TOP_LEVEL_SEGMENT);
Val* result = nullptr;
try {
result = compile_error_guard(code, fe.get());
} catch (DebugFileDeclareException& de) {
// (declare-file (debug)) will throw this exception. the reason for this is so that we can
// wrap the entire source code in a (when *debug-segment* ... ) and compile that version
// instead. therefore, it is recommended to put that declaration as early as possible so that
// the compiler doesn't waste much time.
// the actual source code is (top-level ...) right now though so we need some tricks.
code.as_pair()->cdr = PairObject::make_new(
PairObject::make_new(SymbolObject::make_new(m_goos.reader.symbolTable, "when"),
PairObject::make_new(SymbolObject::make_new(m_goos.reader.symbolTable,
"*debug-segment*"),
code.as_pair()->cdr)),
Object::make_empty_list());
result = compile_error_guard(code, fe.get());
}
// only move to return register if we actually got a result
if (!dynamic_cast<const None*>(result)) {
fe->emit_ir<IR_Return>(code, fe->make_gpr(result->type()), result->to_gpr(code, fe.get()),
emitter::gRegInfo.get_gpr_ret_reg());
}
if (!fe->code().empty()) {
fe->emit_ir<IR_Null>(code);
}
fe->finish();
return fe;
}
Val* Compiler::compile_error_guard(const goos::Object& code, Env* env) {
try {
return compile(code, env);
} catch (CompilerException& ce) {
if (ce.print_err_stack) {
bool term;
auto loc_info = m_goos.reader.db.get_info_for(code, &term);
if (term) {
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Location:\n");
lg::print(loc_info);
}
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Code:\n");
lg::print("{}\n", pretty_print::to_string(code, 120));
if (term) {
ce.print_err_stack = false;
}
std::string line(80, '-');
line.push_back('\n');
lg::print(line);
}
throw ce;
}
catch (std::runtime_error& e) {
lg::print(fg(fmt::color::crimson) | fmt::emphasis::bold, "-- Compilation Error! --\n");
lg::print(fmt::emphasis::bold, "{}\n", e.what());
bool term;
auto loc_info = m_goos.reader.db.get_info_for(code, &term);
if (term) {
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Location:\n");
lg::print(loc_info);
}
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Code:\n");
lg::print("{}\n", pretty_print::to_string(code, 120));
CompilerException ce("Compiler Exception");
if (term) {
ce.print_err_stack = false;
}
std::string line(80, '-');
line.push_back('\n');
lg::print(line);
throw ce;
}
}
void Compiler::color_object_file(FileEnv* env) {
int num_spills_in_file = 0;
for (auto& f : env->functions()) {
AllocationInput input;
input.is_asm_function = f->is_asm_func;
for (auto& i : f->code()) {
input.instructions.push_back(i->to_rai());
input.debug_instruction_names.push_back(i->print());
}
for (auto& reg_val : f->reg_vals()) {
if (reg_val->forced_on_stack()) {
input.force_on_stack_regs.insert(reg_val->ireg().id);
}
}
input.max_vars = f->max_vars();
input.constraints = f->constraints();
input.stack_slots_for_stack_vars = f->stack_slots_used_for_stack_vars();
input.function_name = f->name();
if (m_settings.debug_print_regalloc) {
input.debug_settings.print_input = true;
input.debug_settings.print_result = true;
input.debug_settings.print_analysis = true;
input.debug_settings.allocate_log_level = 2;
}
m_debug_stats.total_funcs++;
auto regalloc_result_2 = allocate_registers_v2(input);
if (regalloc_result_2.ok) {
if (regalloc_result_2.num_spilled_vars > 0) {
// lg::print("Function {} has {} spilled vars.\n", f->name(),
// regalloc_result_2.num_spilled_vars);
}
num_spills_in_file += regalloc_result_2.num_spills;
f->set_allocations(std::move(regalloc_result_2));
} else {
lg::print(
"Warning: function {} failed register allocation with the v2 allocator. Falling back to "
"the v1 allocator.\n",
f->name());
m_debug_stats.funcs_requiring_v1_allocator++;
auto regalloc_result = allocate_registers(input);
m_debug_stats.num_spills_v1 += regalloc_result.num_spills;
num_spills_in_file += regalloc_result.num_spills;
f->set_allocations(std::move(regalloc_result));
}
}
m_debug_stats.num_spills += num_spills_in_file;
}
std::vector<u8> Compiler::codegen_object_file(FileEnv* env) {
try {
auto debug_info = &m_debugger.get_debug_info_for_object(env->name());
debug_info->clear();
CodeGenerator gen(env, debug_info, m_version);
bool ok = true;
auto result = gen.run(&m_ts);
for (auto& f : env->functions()) {
if (f->settings.print_asm) {
lg::print("{}\n", debug_info->disassemble_function_by_name(f->name(), &ok, &m_goos.reader));
}
}
auto stats = gen.get_obj_stats();
m_debug_stats.num_moves_eliminated += stats.moves_eliminated;
env->cleanup_after_codegen();
return result;
} catch (std::exception& e) {
throw_compiler_error_no_code("Error during codegen: {}", e.what());
}
return {};
}
bool Compiler::codegen_and_disassemble_object_file(FileEnv* env,
std::vector<u8>* data_out,
std::string* asm_out) {
auto debug_info = &m_debugger.get_debug_info_for_object(env->name());
debug_info->clear();
CodeGenerator gen(env, debug_info, m_version);
*data_out = gen.run(&m_ts);
bool ok = true;
*asm_out = debug_info->disassemble_all_functions(&ok, &m_goos.reader);
return ok;
}
bool Compiler::connect_to_target() {
if (!m_listener.is_connected()) {
for (int i = 0; i < 1000; i++) {
m_listener.connect_to_target();
std::this_thread::sleep_for(std::chrono::microseconds(10000));
if (m_listener.is_connected()) {
break;
}
}
if (!m_listener.is_connected()) {
return false;
}
}
return true;
}
void Compiler::typecheck(const goos::Object& form,
const TypeSpec& expected,
const TypeSpec& actual,
const std::string& error_message) {
(void)form;
if (!m_ts.typecheck_and_throw(expected, actual, error_message, false, false, true)) {
throw_compiler_error(form, "Typecheck failed. For {}, got a \"{}\" when expecting a \"{}\"",
error_message, actual.print(), expected.print());
}
}
/*!
* Like typecheck, but will allow Val* to be #f if the destination isn't a number.
* Also will convert to register types for the type checking.
*/
void Compiler::typecheck_reg_type_allow_false(const goos::Object& form,
const TypeSpec& expected,
const Val* actual,
const std::string& error_message) {
if (!m_ts.typecheck_and_throw(m_ts.make_typespec("number"), expected, "", false, false)) {
auto as_sym_val = dynamic_cast<const SymbolVal*>(actual);
if (as_sym_val && as_sym_val->name() == "#f") {
return;
}
}
typecheck(form, expected, coerce_to_reg_type(actual->type()), error_message);
}
void Compiler::setup_goos_forms() {
m_goos.register_form("get-enum-vals", [&](const goos::Object& form, goos::Arguments& args,
const std::shared_ptr<goos::EnvironmentObject>& env) {
m_goos.eval_args(&args, env);
va_check(form, args, {goos::ObjectType::SYMBOL}, {});
std::vector<Object> enum_vals;
const auto& enum_name = args.unnamed.at(0).as_symbol()->name;
auto enum_type = m_ts.try_enum_lookup(enum_name);
if (!enum_type) {
throw_compiler_error(form, "Unknown enum {} in get-enum-vals", enum_name);
}
std::vector<std::pair<std::string, s64>> sorted_values;
for (auto& val : enum_type->entries()) {
sorted_values.emplace_back(val.first, val.second);
}
std::sort(sorted_values.begin(), sorted_values.end(),
[](const std::pair<std::string, s64>& a, const std::pair<std::string, s64>& b) {
return a.second < b.second;
});
for (auto& thing : sorted_values) {
enum_vals.push_back(PairObject::make_new(m_goos.intern(thing.first),
goos::Object::make_integer(thing.second)));
}
return goos::build_list(enum_vals);
});
}
void Compiler::asm_file(const CompilationOptions& options) {
// If the filename provided is not a valid path but it's a name (with or without an extension)
// attempt to find it in the defined `asmFileSearchDirs`
//
// For example - (ml "process-drawable.gc")
// - This allows you to load a file without precisely defining the entire path
//
// If multiple candidates are found, abort
std::string file_name = options.filename;
std::string file_path = file_util::get_file_path({file_name});
if (!file_util::file_exists(file_path)) {
if (file_path.empty()) {
lg::print("ERROR - can't load a file without a providing a path\n");
return;
} else if (m_repl && m_repl->repl_config.asm_file_search_dirs.empty()) {
lg::print(
"ERROR - can't load a file that doesn't exist - '{}' and no search dirs are defined\n",
file_path);
return;
}
std::string base_name = file_util::base_name_no_ext(file_path);
// Attempt the find the full path of the file (ignore extension)
std::vector<fs::path> candidate_paths = {};
if (m_repl) {
for (const auto& dir : m_repl->repl_config.asm_file_search_dirs) {
std::string base_dir = file_util::get_file_path({dir});
const auto& results = file_util::find_files_recursively(
base_dir, std::regex(fmt::format("^{}(\\..*)?$", base_name)));
for (const auto& result : results) {
candidate_paths.push_back(result);
}
}
}
if (candidate_paths.empty()) {
lg::print("ERROR - attempt to find object file automatically, but found nothing\n");
return;
} else if (candidate_paths.size() > 1) {
lg::print("ERROR - attempt to find object file automatically, but found multiple\n");
return;
}
// Found the file!, use it!
file_path = candidate_paths.at(0).string();
}
auto code = m_goos.reader.read_from_file({file_path});
std::string obj_file_name = file_path;
// Extract object name from file name.
for (int idx = int(file_path.size()) - 1; idx-- > 0;) {
if (file_path.at(idx) == '\\' || file_path.at(idx) == '/') {
obj_file_name = file_path.substr(idx + 1);
break;
}
}
obj_file_name = obj_file_name.substr(0, obj_file_name.find_last_of('.'));
// COMPILE
auto obj_file = compile_object_file(obj_file_name, code, !options.no_code);
if (options.color) {
// register allocation
color_object_file(obj_file);
// code/object file generation
std::vector<u8> data;
std::string disasm;
if (options.disassemble) {
codegen_and_disassemble_object_file(obj_file, &data, &disasm);
if (options.disassembly_output_file.empty()) {
printf("%s\n", disasm.c_str());
} else {
file_util::write_text_file(options.disassembly_output_file, disasm);
}
} else {
data = codegen_object_file(obj_file);
}
// send to target
if (options.load) {
if (m_listener.is_connected()) {
m_listener.send_code(data, obj_file_name);
} else {
printf("WARNING - couldn't load because listener isn't connected\n"); // todo log warn
}
}
// save file
if (options.write) {
auto path = file_util::get_jak_project_dir() / "out" / m_make.compiler_output_prefix() /
"obj" / (obj_file_name + ".o");
file_util::create_dir_if_needed_for_file(path);
file_util::write_binary_file(path, (void*)data.data(), data.size());
}
} else {
if (options.load) {
printf("WARNING - couldn't load because coloring is not enabled\n");
}
if (options.write) {
printf("WARNING - couldn't write because coloring is not enabled\n");
}
if (options.disassemble) {
printf("WARNING - couldn't disassemble because coloring is not enabled\n");
}
}
}