mirror of
https://github.com/open-goal/jak-project.git
synced 2024-10-20 11:26:18 -04:00
10ac78200b
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
516 lines
17 KiB
C++
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");
|
|
}
|
|
}
|
|
}
|