Files
jak-project/goalc/compiler/Compiler.cpp
T
ManDude 324def1303 split new pc features in some files into their own code files + address some old issues + ripple graphics improvements (#2216)
Moves PC-specific entity and debug menu things to `entity-debug.gc` and
`default-menu-pc.gc` respectively and makes `(declare-file (debug))`
work as it should (no need to wrap the entire file in `(when
*debug-segment*` now!).

Also changes the DGO descriptor format so that it's less verbose. It
might break custom levels, but the format change is very simple so it
should not be difficult for anyone to update to the new format. Sadly,
you lose the completely useless ability to use DGO object names that
don't match the source file name. The horror!

I've also gone ahead and expanded the force envmap option to also force
the ripple effect to be active. I did not notice any performance or
visual drawbacks from this. Gets rid of some distracting LOD and some
water pools appearing super flat (and pitch back for dark eco).

Fixes #1424
2023-02-13 21:39:14 +00:00

511 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::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(user_profile) {
m_listener.add_debugger(&m_debugger);
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 (auto& builtin : g_goal_forms) {
m_symbol_info.add_builtin(builtin.first);
}
// 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;
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");
}
}
}