mirror of
https://github.com/open-goal/jak-project.git
synced 2024-10-20 21:27:52 -04:00
0b8a878533
* fix diassembly * update changelog * fix exit crash * doc
544 lines
17 KiB
C++
544 lines
17 KiB
C++
/*!
|
|
* @file CompilerControl.cpp
|
|
* Compiler implementation for forms which actually control the compiler.
|
|
*/
|
|
|
|
#include <filesystem>
|
|
#include "goalc/compiler/Compiler.h"
|
|
#include "goalc/compiler/IR.h"
|
|
#include "common/util/Timer.h"
|
|
#include "common/util/DgoWriter.h"
|
|
#include "common/util/FileUtil.h"
|
|
#include "goalc/data_compiler/game_text.h"
|
|
#include "goalc/data_compiler/game_count.h"
|
|
#include "common/goos/ReplUtils.h"
|
|
#include <regex>
|
|
#include <stack>
|
|
|
|
/*!
|
|
* Exit the compiler. Disconnects the listener and tells the target to reset itself.
|
|
* Will actually exit the next time the REPL runs.
|
|
*/
|
|
Val* Compiler::compile_exit(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {}, {});
|
|
|
|
if (m_debugger.is_attached()) {
|
|
m_debugger.detach();
|
|
}
|
|
|
|
if (m_listener.is_connected()) {
|
|
m_listener.send_reset(false);
|
|
}
|
|
// flag for the REPL.
|
|
m_want_exit = true;
|
|
if (m_repl) {
|
|
m_repl->save_history();
|
|
}
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Evaluate GOOS code. It's not possible to get the result, so this is really only useful to get
|
|
* a side effect. Used to bootstrap the GOAL/GOOS macro system.
|
|
*/
|
|
Val* Compiler::compile_seval(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
try {
|
|
for_each_in_list(rest, [&](const goos::Object& o) {
|
|
m_goos.eval_with_rewind(o, m_goos.global_environment.as_env());
|
|
});
|
|
} catch (std::runtime_error& e) {
|
|
throw_compiler_error(form, "Error while evaluating GOOS: ", e.what());
|
|
}
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Compile a "data file"
|
|
*/
|
|
Val* Compiler::compile_asm_data_file(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {goos::ObjectType::SYMBOL, goos::ObjectType::STRING}, {});
|
|
auto kind = symbol_string(args.unnamed.at(0));
|
|
if (kind == "game-text") {
|
|
compile_game_text(as_string(args.unnamed.at(1)));
|
|
} else if (kind == "game-count") {
|
|
compile_game_count(as_string(args.unnamed.at(1)));
|
|
} else {
|
|
throw_compiler_error(form, "The option {} was not recognized for asm-data-file.", kind);
|
|
}
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Compile a file, and optionally color, save, or load.
|
|
* This should only be used for v3 "code object" files.
|
|
*/
|
|
Val* Compiler::compile_asm_file(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
int i = 0;
|
|
std::string filename;
|
|
bool load = false;
|
|
bool color = false;
|
|
bool write = false;
|
|
bool no_code = false;
|
|
bool disassemble = false;
|
|
|
|
std::vector<std::pair<std::string, double>> timing;
|
|
Timer total_timer;
|
|
|
|
// parse arguments
|
|
for_each_in_list(rest, [&](const goos::Object& o) {
|
|
if (i == 0) {
|
|
filename = as_string(o);
|
|
} else {
|
|
auto setting = symbol_string(o);
|
|
if (setting == ":load") {
|
|
load = true;
|
|
} else if (setting == ":color") {
|
|
color = true;
|
|
} else if (setting == ":write") {
|
|
write = true;
|
|
} else if (setting == ":no-code") {
|
|
no_code = true;
|
|
} else if (setting == ":disassemble") {
|
|
disassemble = true;
|
|
} else {
|
|
throw_compiler_error(form, "The option {} was not recognized for asm-file.", setting);
|
|
}
|
|
}
|
|
i++;
|
|
});
|
|
|
|
// READ
|
|
Timer reader_timer;
|
|
auto code = m_goos.reader.read_from_file({filename});
|
|
timing.emplace_back("read", reader_timer.getMs());
|
|
|
|
Timer compile_timer;
|
|
std::string obj_file_name = filename;
|
|
|
|
// Extract object name from file name.
|
|
for (int idx = int(filename.size()) - 1; idx-- > 0;) {
|
|
if (filename.at(idx) == '\\' || filename.at(idx) == '/') {
|
|
obj_file_name = filename.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, !no_code);
|
|
timing.emplace_back("compile", compile_timer.getMs());
|
|
|
|
if (color) {
|
|
// register allocation
|
|
Timer color_timer;
|
|
color_object_file(obj_file);
|
|
timing.emplace_back("color", color_timer.getMs());
|
|
|
|
// code/object file generation
|
|
Timer codegen_timer;
|
|
std::vector<u8> data;
|
|
std::string disasm;
|
|
if (disassemble) {
|
|
codegen_and_disassemble_object_file(obj_file, &data, &disasm);
|
|
printf("%s\n", disasm.c_str());
|
|
} else {
|
|
data = codegen_object_file(obj_file);
|
|
}
|
|
timing.emplace_back("codegen", codegen_timer.getMs());
|
|
|
|
// send to target
|
|
if (load) {
|
|
if (m_listener.is_connected()) {
|
|
m_listener.send_code(data);
|
|
} else {
|
|
printf("WARNING - couldn't load because listener isn't connected\n"); // todo log warn
|
|
}
|
|
}
|
|
|
|
// save file
|
|
if (write) {
|
|
file_util::create_dir_if_needed(file_util::get_file_path({"out", "obj"}));
|
|
file_util::write_binary_file(file_util::get_file_path({"out", "obj", obj_file_name + ".o"}),
|
|
(void*)data.data(), data.size());
|
|
}
|
|
} else {
|
|
if (load) {
|
|
printf("WARNING - couldn't load because coloring is not enabled\n");
|
|
}
|
|
|
|
if (write) {
|
|
printf("WARNING - couldn't write because coloring is not enabled\n");
|
|
}
|
|
|
|
if (disassemble) {
|
|
printf("WARNING - couldn't disassemble because coloring is not enabled\n");
|
|
}
|
|
}
|
|
|
|
if (m_settings.print_timing) {
|
|
printf("F: %36s ", obj_file_name.c_str());
|
|
timing.emplace_back("total", total_timer.getMs());
|
|
for (auto& e : timing) {
|
|
printf(" %12s %4.0f", e.first.c_str(), e.second);
|
|
}
|
|
printf("\n");
|
|
} else {
|
|
auto total_time = total_timer.getMs();
|
|
if (total_time > 10.0 && color) {
|
|
fmt::print("[ASM-FILE] {} took {:.2f} ms\n", obj_file_name, total_time);
|
|
}
|
|
}
|
|
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Simple help / documentation command
|
|
*/
|
|
Val* Compiler::compile_repl_help(const goos::Object&, const goos::Object&, Env*) {
|
|
m_repl.get()->print_help_message();
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Connect the compiler to a target. Takes an optional IP address / port, defaults to
|
|
* 127.0.0.1 and 8112, which is the local computer and the default port for the DECI2 over IP
|
|
* implementation.
|
|
*/
|
|
Val* Compiler::compile_listen_to_target(const goos::Object& form,
|
|
const goos::Object& rest,
|
|
Env* env) {
|
|
(void)env;
|
|
std::string ip = "127.0.0.1";
|
|
int port = DECI2_PORT;
|
|
bool got_port = false, got_ip = false;
|
|
|
|
for_each_in_list(rest, [&](const goos::Object& o) {
|
|
if (o.is_string()) {
|
|
if (got_ip) {
|
|
throw_compiler_error(form, "listen-to-target can only use 1 IP address");
|
|
}
|
|
got_ip = true;
|
|
ip = o.as_string()->data;
|
|
} else if (o.is_int()) {
|
|
if (got_port) {
|
|
throw_compiler_error(form, "listen-to-target can only use 1 port number");
|
|
}
|
|
got_port = true;
|
|
port = o.integer_obj.value;
|
|
} else {
|
|
throw_compiler_error(form, "invalid argument to listen-to-target: \"{}\"", o.print());
|
|
}
|
|
});
|
|
|
|
m_listener.connect_to_target(30, ip, port);
|
|
return get_none();
|
|
}
|
|
|
|
Val* Compiler::compile_repl_clear_screen(const goos::Object&, const goos::Object&, Env*) {
|
|
m_repl.get()->clear_screen();
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Send the target a command to reset, which totally resets the state of the target.
|
|
* Optionally takes a :shutdown command which causes the exec_runtime function of the target
|
|
* to return after MachineShutdown.
|
|
*/
|
|
Val* Compiler::compile_reset_target(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
bool shutdown = false;
|
|
for_each_in_list(rest, [&](const goos::Object& o) {
|
|
if (o.is_symbol() && symbol_string(o) == ":shutdown") {
|
|
shutdown = true;
|
|
} else {
|
|
throw_compiler_error(form, "invalid argument to reset-target: \"{}\"", o.print());
|
|
}
|
|
});
|
|
m_listener.send_reset(shutdown);
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Send a "poke" message to the target. This can be used to check if the target is still alive and
|
|
* acknowledges commands, and also tells that target that somebody is connected so it will flush
|
|
* its outgoing buffers that have been storing data from startup.
|
|
*/
|
|
Val* Compiler::compile_poke(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {}, {});
|
|
m_listener.send_poke();
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Enter a goos REPL.
|
|
*/
|
|
Val* Compiler::compile_gs(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {}, {});
|
|
m_goos.execute_repl(*m_repl.get());
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Set a compiler setting by name.
|
|
*/
|
|
Val* Compiler::compile_set_config(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {goos::ObjectType::SYMBOL, {}}, {});
|
|
m_settings.set(symbol_string(args.unnamed.at(0)), args.unnamed.at(1));
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Ignore the "in-package" statement and anything it contains at the top of GOAL files.
|
|
*/
|
|
Val* Compiler::compile_in_package(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)form;
|
|
(void)rest;
|
|
(void)env;
|
|
return get_none();
|
|
}
|
|
|
|
/*!
|
|
* Build dgo files. Takes a string argument pointing to the DGO description file, which is read
|
|
* and parsed here.
|
|
*/
|
|
Val* Compiler::compile_build_dgo(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {goos::ObjectType::STRING}, {});
|
|
auto dgo_desc = pair_cdr(m_goos.reader.read_from_file({args.unnamed.at(0).as_string()->data}));
|
|
|
|
for_each_in_list(dgo_desc, [&](const goos::Object& dgo) {
|
|
DgoDescription desc;
|
|
auto first = pair_car(dgo);
|
|
desc.dgo_name = as_string(first);
|
|
auto dgo_rest = pair_cdr(dgo);
|
|
|
|
for_each_in_list(dgo_rest, [&](const goos::Object& entry) {
|
|
auto e_arg = get_va(dgo, entry);
|
|
va_check(dgo, e_arg, {goos::ObjectType::STRING, goos::ObjectType::STRING}, {});
|
|
DgoDescription::DgoEntry o;
|
|
o.file_name = as_string(e_arg.unnamed.at(0));
|
|
o.name_in_dgo = as_string(e_arg.unnamed.at(1));
|
|
if (o.file_name.substr(o.file_name.length() - 3) != ".go") {
|
|
desc.entries.push_back(o);
|
|
} else {
|
|
// allow data objects to be missing.
|
|
if (std::filesystem::exists(file_util::get_file_path({"out", "obj", o.file_name}))) {
|
|
desc.entries.push_back(o);
|
|
}
|
|
}
|
|
});
|
|
|
|
build_dgo(desc);
|
|
});
|
|
|
|
return get_none();
|
|
}
|
|
|
|
Val* Compiler::compile_reload(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {}, {});
|
|
m_want_reload = true;
|
|
return get_none();
|
|
}
|
|
|
|
std::string Compiler::make_symbol_info_description(const SymbolInfo& info) {
|
|
switch (info.kind()) {
|
|
case SymbolInfo::Kind::GLOBAL_VAR:
|
|
return fmt::format("[Global Variable] Type: {} Defined: {}",
|
|
m_symbol_types.at(info.name()).print(),
|
|
m_goos.reader.db.get_info_for(info.src_form()));
|
|
case SymbolInfo::Kind::LANGUAGE_BUILTIN:
|
|
return fmt::format("[Built-in Form] {}\n", info.name());
|
|
case SymbolInfo::Kind::METHOD:
|
|
return fmt::format("[Method] Type: {} Method Name: {} Defined: {}", info.type(), info.name(),
|
|
m_goos.reader.db.get_info_for(info.src_form()));
|
|
case SymbolInfo::Kind::TYPE:
|
|
return fmt::format("[Type] Name: {} Defined: {}", info.name(),
|
|
m_goos.reader.db.get_info_for(info.src_form()));
|
|
case SymbolInfo::Kind::MACRO:
|
|
return fmt::format("[Macro] Name: {} Defined: {}", info.name(),
|
|
m_goos.reader.db.get_info_for(info.src_form()));
|
|
case SymbolInfo::Kind::CONSTANT:
|
|
return fmt::format(
|
|
"[Constant] Name: {} Value: {} Defined: {}", info.name(),
|
|
m_global_constants.at(m_goos.reader.symbolTable.intern(info.name())).print(),
|
|
m_goos.reader.db.get_info_for(info.src_form()));
|
|
case SymbolInfo::Kind::FUNCTION:
|
|
return fmt::format("[Function] Name: {} Defined: {}", info.name(),
|
|
m_goos.reader.db.get_info_for(info.src_form()));
|
|
case SymbolInfo::Kind::FWD_DECLARED_SYM:
|
|
return fmt::format("[Forward-Declared] Name: {} Defined: {}", info.name(),
|
|
m_goos.reader.db.get_info_for(info.src_form()));
|
|
default:
|
|
assert(false);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
Val* Compiler::compile_get_info(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {goos::ObjectType::SYMBOL}, {});
|
|
|
|
auto result = m_symbol_info.lookup_exact_name(args.unnamed.at(0).as_symbol()->name);
|
|
if (!result) {
|
|
fmt::print("No results found.\n");
|
|
} else {
|
|
for (auto& info : *result) {
|
|
fmt::print("{}", make_symbol_info_description(info));
|
|
}
|
|
}
|
|
|
|
return get_none();
|
|
}
|
|
|
|
Replxx::completions_t Compiler::find_symbols_by_prefix(std::string const& context,
|
|
int& contextLen,
|
|
std::vector<std::string> const& user_data) {
|
|
(void)contextLen;
|
|
(void)user_data;
|
|
auto token = m_repl.get()->get_current_repl_token(context);
|
|
auto possible_forms = m_symbol_info.lookup_symbols_starting_with(token.first);
|
|
Replxx::completions_t completions;
|
|
for (auto& x : possible_forms) {
|
|
completions.push_back(token.second ? "(" + x : x);
|
|
}
|
|
return completions;
|
|
}
|
|
|
|
Replxx::hints_t Compiler::find_hints_by_prefix(std::string const& context,
|
|
int& contextLen,
|
|
Replxx::Color& color,
|
|
std::vector<std::string> const& user_data) {
|
|
(void)contextLen;
|
|
(void)user_data;
|
|
auto token = m_repl.get()->get_current_repl_token(context);
|
|
auto possible_forms = m_symbol_info.lookup_symbols_starting_with(token.first);
|
|
|
|
Replxx::hints_t hints;
|
|
|
|
// Only show hints if there are <= 3 possibilities
|
|
if (possible_forms.size() <= 3) {
|
|
for (auto& x : possible_forms) {
|
|
hints.push_back(token.second ? "(" + x : x);
|
|
}
|
|
}
|
|
|
|
// set hint color to green if single match found
|
|
if (hints.size() == 1) {
|
|
color = Replxx::Color::GREEN;
|
|
}
|
|
|
|
return hints;
|
|
}
|
|
|
|
void Compiler::repl_coloring(
|
|
std::string const& context,
|
|
Replxx::colors_t& colors,
|
|
std::vector<std::pair<std::string, Replxx::Color>> const& regex_color) {
|
|
(void)regex_color;
|
|
using cl = Replxx::Color;
|
|
// TODO - a proper circular queue would be cleaner to use
|
|
std::deque<cl> paren_colors = {cl::GREEN, cl::CYAN, cl::MAGENTA};
|
|
std::stack<std::pair<char, cl>> expression_stack;
|
|
|
|
std::pair<int, std::string> curr_symbol = {-1, ""};
|
|
for (std::string::size_type i = 0; i < context.size(); i++) {
|
|
char curr = context.at(i);
|
|
// We lookup every potential symbol and color it based on it's type
|
|
if (std::isspace(curr) || curr == ')') {
|
|
// Lookup the symbol, if its legit, color it
|
|
if (!curr_symbol.second.empty() && curr_symbol.second.at(0) == '(') {
|
|
curr_symbol.second.erase(0, 1);
|
|
curr_symbol.first++;
|
|
}
|
|
std::vector<SymbolInfo>* sym_match = m_symbol_info.lookup_exact_name(curr_symbol.second);
|
|
if (sym_match != nullptr && sym_match->size() == 1) {
|
|
SymbolInfo sym_info = sym_match->at(0);
|
|
for (int pos = curr_symbol.first; pos <= int(i); pos++) {
|
|
// TODO - currently just coloring all types brown/gold
|
|
// - would be nice to have a different color for globals, functions, etc
|
|
colors.at(pos) = cl::BROWN;
|
|
}
|
|
}
|
|
curr_symbol = {-1, ""};
|
|
} else {
|
|
if (curr_symbol.first == -1) {
|
|
curr_symbol.first = i;
|
|
}
|
|
curr_symbol.second += curr;
|
|
}
|
|
// Rainbow paren coloring and known-form coloring
|
|
if (curr == '(') {
|
|
cl color = paren_colors.front();
|
|
expression_stack.push({curr, color});
|
|
colors.at(i) = color;
|
|
paren_colors.pop_front();
|
|
paren_colors.push_back(color);
|
|
} else if (curr == ')') {
|
|
if (expression_stack.empty()) {
|
|
colors.at(i) = cl::RED;
|
|
} else {
|
|
auto& matching_paren = expression_stack.top();
|
|
expression_stack.pop();
|
|
if (matching_paren.first == '(') {
|
|
if (i == context.size() - 1 && !expression_stack.empty()) {
|
|
colors.at(i) = cl::RED;
|
|
} else {
|
|
colors.at(i) = matching_paren.second;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Reset the color order
|
|
if (expression_stack.empty()) {
|
|
paren_colors = {cl::GREEN, cl::CYAN, cl::MAGENTA};
|
|
}
|
|
}
|
|
|
|
// TODO - general syntax highlighting with regexes (quotes, symbols, etc)
|
|
}
|
|
|
|
Val* Compiler::compile_autocomplete(const goos::Object& form, const goos::Object& rest, Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {goos::ObjectType::SYMBOL}, {});
|
|
|
|
Timer timer;
|
|
auto result = m_symbol_info.lookup_symbols_starting_with(args.unnamed.at(0).as_symbol()->name);
|
|
auto time = timer.getMs();
|
|
|
|
for (auto& x : result) {
|
|
fmt::print(" {}\n", x);
|
|
}
|
|
|
|
fmt::print("Autocomplete: {}/{} symbols matched, took {:.2f} ms\n", result.size(),
|
|
m_symbol_info.symbol_count(), time);
|
|
|
|
return get_none();
|
|
}
|
|
|
|
Val* Compiler::compile_add_macro_to_autocomplete(const goos::Object& form,
|
|
const goos::Object& rest,
|
|
Env* env) {
|
|
(void)env;
|
|
auto args = get_va(form, rest);
|
|
va_check(form, args, {goos::ObjectType::SYMBOL}, {});
|
|
m_symbol_info.add_macro(args.unnamed.at(0).as_symbol()->name, form);
|
|
return get_none();
|
|
} |