goalc/repl: cleanup of goalc/REPL code and some QoL improvements (#2104)

- lets you split up your `startup.gc` file into two sections
  - one that runs on initial startup / reloads
  - the other that runs when you listen to a target
- allows for customization of the keybinds added a month or so ago
- removes a useless flag (--startup-cmd) and marks others for
deprecation.
- added another help prompt that lists all the keybinds and what they do

Co-authored-by: water <awaterford111445@gmail.com>
This commit is contained in:
Tyler Wilding
2023-01-07 11:24:02 -05:00
committed by GitHub
parent 2f4146d469
commit 00ac12094e
35 changed files with 555 additions and 254 deletions
-7
View File
@@ -141,13 +141,6 @@
"name": "REPL - Jak 2",
"args": ["--user-auto", "--game", "jak2"]
},
{
"type": "default",
"project": "CMakeLists.txt",
"projectTarget": "goalc.exe (bin\\goalc.exe)",
"name": "REPL - Auto Listen",
"args": ["--user-auto", "--auto-lt"]
},
{
"type": "default",
"project": "CMakeLists.txt",
-3
View File
@@ -68,9 +68,6 @@ tasks:
msg: "Couldn't locate compiler executable in '{{.GOALC_BIN_RELEASE_DIR}}/goalc'"
cmds:
- "{{.GOALC_BIN_RELEASE_DIR}}/goalc --user-auto --game {{.GAME}}"
repl-lt:
cmds:
- "{{.GOALC_BIN_RELEASE_DIR}}/goalc --auto-lt --user-auto --game {{.GAME}}"
format:
desc: "Format code"
cmds:
+4 -3
View File
@@ -19,11 +19,12 @@ add_library(common
goos/PrettyPrinter2.cpp
goos/Reader.cpp
goos/TextDB.cpp
goos/ReplUtils.cpp
repl/config.cpp
repl/util.cpp
log/log.cpp
math/geometry.cpp
nrepl/ReplClient.cpp
nrepl/ReplServer.cpp
repl/nrepl/ReplClient.cpp
repl/nrepl/ReplServer.cpp
type_system/defenum.cpp
type_system/deftype.cpp
type_system/state.cpp
+1 -1
View File
@@ -12,7 +12,7 @@
#include <WinSock2.h>
#include <WS2tcpip.h>
#endif
#include "common/nrepl/ReplServer.h"
#include "common/repl/nrepl/ReplServer.h"
#include "third-party/fmt/core.h"
// clang-format on
+1 -1
View File
@@ -160,7 +160,7 @@ HeapObject* Interpreter::intern_ptr(const std::string& name) {
/*!
* Display the REPL, which will run until the user executes exit.
*/
void Interpreter::execute_repl(ReplWrapper& repl) {
void Interpreter::execute_repl(REPL::Wrapper& repl) {
want_exit = false;
while (!want_exit) {
try {
+1 -1
View File
@@ -16,7 +16,7 @@ class Interpreter {
public:
Interpreter(const std::string& user_profile = "#f");
~Interpreter();
void execute_repl(ReplWrapper& repl);
void execute_repl(REPL::Wrapper& repl);
void throw_eval_error(const Object& o, const std::string& err);
Object eval_with_rewind(const Object& obj, const std::shared_ptr<EnvironmentObject>& env);
bool get_global_variable_by_name(const std::string& name, Object* dest);
+2 -3
View File
@@ -11,9 +11,8 @@
#include "Reader.h"
#include "ReplUtils.h"
#include "common/log/log.h"
#include "common/repl/util.h"
#include "common/util/FileUtil.h"
#include "common/util/FontUtils.h"
@@ -195,7 +194,7 @@ bool Reader::is_valid_source_char(char c) const {
/*!
* Prompt the user and read the result.
*/
std::optional<Object> Reader::read_from_stdin(const std::string& prompt, ReplWrapper& repl) {
std::optional<Object> Reader::read_from_stdin(const std::string& prompt, REPL::Wrapper& repl) {
// escape code will make sure that we remove any color
std::string prompt_full = "\033[0m" + prompt;
+2 -3
View File
@@ -16,10 +16,9 @@
#include <unordered_map>
#include <utility>
#include "ReplUtils.h"
#include "common/goos/Object.h"
#include "common/goos/TextDB.h"
#include "common/repl/util.h"
#include "common/util/Assert.h"
namespace goos {
@@ -74,7 +73,7 @@ class Reader {
Object read_from_string(const std::string& str,
bool add_top_level = true,
const std::optional<std::string>& string_name = {});
std::optional<Object> read_from_stdin(const std::string& prompt, ReplWrapper& repl);
std::optional<Object> read_from_stdin(const std::string& prompt, REPL::Wrapper& repl);
Object read_from_file(const std::vector<std::string>& file_path, bool check_encoding = false);
bool check_string_is_valid(const std::string& str) const;
-37
View File
@@ -1,37 +0,0 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
#include "third-party/replxx/include/replxx.hxx"
using Replxx = replxx::Replxx;
class ReplWrapper {
Replxx repl;
public:
ReplWrapper() {}
Replxx& get_repl() { return repl; }
void init_default_settings();
// Functionality / Commands
void clear_screen();
void print_to_repl(const std::string_view& str);
void print_welcome_message();
void set_history_max_size(size_t len);
const char* readline(const std::string& prompt);
void add_to_history(const std::string& line);
void save_history();
void load_history();
void print_help_message();
std::pair<std::string, bool> get_current_repl_token(std::string const& context);
std::vector<std::string> examples{};
using cl = Replxx::Color;
std::vector<std::pair<std::string, cl>> regex_colors{};
private:
replxx::Replxx::key_press_handler_t commit_text_action(std::string text_to_commit);
};
+95
View File
@@ -0,0 +1,95 @@
#include "config.h"
#include "common/versions.h"
#include "third-party/fmt/core.h"
namespace REPL {
void to_json(json& j, const Config& obj) {
j = json{
{"numConnectToTargetAttempts", obj.target_connect_attempts},
{"asmFileSearchDirs", obj.asm_file_search_dirs},
{"keybinds", obj.keybinds},
};
}
void from_json(const json& j, Config& obj) {
if (j.contains("numConnectToTargetAttempts")) {
j.at("numConnectToTargetAttempts").get_to(obj.target_connect_attempts);
}
if (j.contains("asmFileSearchDirs")) {
j.at("asmFileSearchDirs").get_to(obj.asm_file_search_dirs);
}
if (j.contains("appendKeybinds")) {
j.at("appendKeybinds").get_to(obj.append_keybinds);
}
if (j.contains("keybinds")) {
std::vector<KeyBind> keybinds = j.at("keybinds");
if (!obj.append_keybinds) {
obj.keybinds = keybinds;
} else {
// append the keybinds
// - start with the provided ones
// - skip ones from the default set if they have the same key + modifier combination
for (const auto& default_bind : obj.keybinds) {
// check if it's a duplicate bind
bool duplicate = false;
for (const auto& new_bind : keybinds) {
if (new_bind.key == default_bind.key && new_bind.modifier == default_bind.modifier) {
duplicate = true;
break;
}
}
if (!duplicate) {
keybinds.push_back(default_bind);
}
}
obj.keybinds = keybinds;
}
}
// if there is game specific configuration, override any values we just set
if (j.contains(version_to_game_name(obj.game_version))) {
from_json(j.at(version_to_game_name(obj.game_version)), obj);
}
}
std::string KeyBind::string() const {
switch (modifier) {
case KeyBind::Modifier::CTRL:
return fmt::format("CTRL-{}", key);
case KeyBind::Modifier::SHIFT:
return fmt::format("SHIFT-{}", key);
case KeyBind::Modifier::META:
return fmt::format("META-{}", key);
}
}
void to_json(json& j, const KeyBind& obj) {
j = json{{"description", obj.description}, {"command", obj.command}, {"key", obj.key}};
switch (obj.modifier) {
case KeyBind::Modifier::CTRL:
j["modifier"] = "ctrl";
break;
case KeyBind::Modifier::SHIFT:
j["modifier"] = "shift";
break;
case KeyBind::Modifier::META:
j["modifier"] = "meta";
break;
}
}
void from_json(const json& j, KeyBind& obj) {
j.at("description").get_to(obj.description);
j.at("command").get_to(obj.command);
j.at("key").get_to(obj.key);
auto modString = j.at("modifier").get<std::string>();
if (modString == "ctrl") {
obj.modifier = KeyBind::Modifier::CTRL;
} else if (modString == "shift") {
obj.modifier = KeyBind::Modifier::SHIFT;
} else if (modString == "meta") {
obj.modifier = KeyBind::Modifier::META;
}
}
} // namespace REPL
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "common/versions.h"
#include "third-party/json.hpp"
using json = nlohmann::json;
namespace REPL {
struct KeyBind {
// NOTE - in my experience, meta doesn't work on windows and shift is probably a bad idea when
// typing text! but I leave it up to the user.
enum class Modifier { CTRL, SHIFT, META };
Modifier modifier;
std::string key;
std::string description;
std::string command;
std::string string() const;
};
void to_json(json& j, const KeyBind& obj);
void from_json(const json& j, KeyBind& obj);
struct Config {
GameVersion game_version;
Config(GameVersion _game_version) : game_version(_game_version){};
// this is the default REPL configuration
int target_connect_attempts = 30;
std::vector<std::string> asm_file_search_dirs = {};
bool append_keybinds = true;
std::vector<KeyBind> keybinds = {
{KeyBind::Modifier::CTRL, "T", "Starts up the game runtime", "(test-play)"},
{KeyBind::Modifier::CTRL, "Q", "Exit the REPL", "(e)"},
{KeyBind::Modifier::CTRL, "L", "Listen to an available game process", "(lt)"},
{KeyBind::Modifier::CTRL, "W",
"Halt the attached process so you can re-launch a crashed game", "(:stop)"},
{KeyBind::Modifier::CTRL, "G", "Attach the debugger to the process", "(dbgc)"},
{KeyBind::Modifier::CTRL, "B", "Displays the most recently caught backtrace", "(:di)"},
{KeyBind::Modifier::CTRL, "N", "Full build of the game", "(mi)"}};
};
void to_json(json& j, const Config& obj);
void from_json(const json& j, Config& obj);
} // namespace REPL
@@ -107,8 +107,8 @@ std::optional<std::string> ReplServer::get_msg() {
auto req_bytes = read_from_socket(sock, header_buffer.data(), header_buffer.size());
if (req_bytes == 0) {
// Socket disconnected
// TODO - add a queue of messages in the ReplWrapper so we can print _BEFORE_ the prompt is
// output
// TODO - add a queue of messages in the REPL::Wrapper so we can print _BEFORE_ the prompt
// is output
fmt::print("[nREPL:{}] Client Disconnected: {}\n", tcp_port, inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port), sock);
@@ -1,22 +1,22 @@
#include "ReplUtils.h"
#include "util.h"
#include "common/util/FileUtil.h"
#include "common/util/json_util.h"
#include "common/util/string_util.h"
#include "common/versions.h"
#include "third-party/fmt/color.h"
#include "third-party/fmt/core.h"
#include "third-party/replxx/include/replxx.hxx"
// TODO - expand a list of hints (ie. a hint for defun to show at a glance how to write a function,
// or perhaps, show the docstring for the current function being used?)
using Replxx = replxx::Replxx;
void ReplWrapper::clear_screen() {
namespace REPL {
void Wrapper::clear_screen() {
repl.clear_screen();
}
void ReplWrapper::print_welcome_message() {
void Wrapper::print_welcome_message() {
// TODO - dont print on std-out
// Welcome message / brief intro for documentation
std::string ascii;
@@ -29,9 +29,9 @@ void ReplWrapper::print_welcome_message() {
fmt::print("Welcome to OpenGOAL {}.{}!\n", versions::GOAL_VERSION_MAJOR,
versions::GOAL_VERSION_MINOR);
fmt::print("Run ");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(repl-help)");
fmt::print(" for help with common commands and REPL usage.\n");
fmt::print("Run {} or {} for help with common commands and REPL usage.\n",
fmt::styled("(repl-help)", fmt::emphasis::bold | fg(fmt::color::cyan)),
fmt::styled("(repl-keybinds)", fmt::emphasis::bold | fg(fmt::color::cyan)));
fmt::print("Run ");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(lt)");
fmt::print(" to connect to the local target.\n");
@@ -40,29 +40,29 @@ void ReplWrapper::print_welcome_message() {
fmt::print(" to rebuild the entire game.\n\n");
}
void ReplWrapper::print_to_repl(const std::string_view& str) {
void Wrapper::print_to_repl(const std::string& str) {
repl.print(str.data());
}
void ReplWrapper::set_history_max_size(size_t len) {
void Wrapper::set_history_max_size(size_t len) {
repl.set_max_history_size(len);
}
const char* ReplWrapper::readline(const std::string& prompt) {
const char* Wrapper::readline(const std::string& prompt) {
return repl.input(prompt);
}
void ReplWrapper::add_to_history(const std::string& line) {
void Wrapper::add_to_history(const std::string& line) {
repl.history_add(line);
}
void ReplWrapper::save_history() {
void Wrapper::save_history() {
fs::path path = file_util::get_user_config_dir() / ".opengoal.repl.history";
file_util::create_dir_if_needed_for_file(path.string());
repl.history_save(path.string());
}
void ReplWrapper::load_history() {
void Wrapper::load_history() {
fs::path path = file_util::get_user_config_dir() / ".opengoal.repl.history";
if (fs::exists(path)) {
repl.history_load(path.string());
@@ -71,7 +71,7 @@ void ReplWrapper::load_history() {
}
}
std::pair<std::string, bool> ReplWrapper::get_current_repl_token(std::string const& context) {
std::pair<std::string, bool> Wrapper::get_current_repl_token(std::string const& context) {
// Find the current token
std::string token = "";
for (auto c = context.crbegin(); c != context.crend(); c++) {
@@ -90,7 +90,7 @@ std::pair<std::string, bool> ReplWrapper::get_current_repl_token(std::string con
return {token, false};
}
void ReplWrapper::print_help_message() {
void Wrapper::print_help_message() {
fmt::print(fmt::emphasis::bold, "\nREPL Controls:\n");
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(:clear)\n");
fmt::print(" - Clear the current screen\n");
@@ -130,31 +130,110 @@ void ReplWrapper::print_help_message() {
fmt::print(" - Enter a GOOS REPL\n");
}
replxx::Replxx::key_press_handler_t ReplWrapper::commit_text_action(std::string text_to_commit) {
void Wrapper::print_keybind_help() {
fmt::print(fmt::emphasis::bold, "\nREPL KeyBinds:\n");
for (const auto& bind : repl_config.keybinds) {
fmt::print("{}\n", fmt::styled(bind.string(), fmt::fg(fmt::color::cyan)));
fmt::print("{}\n", fmt::styled(bind.description, fmt::fg(fmt::color::gray)));
}
}
replxx::Replxx::key_press_handler_t Wrapper::commit_text_action(std::string text_to_commit) {
return [this, text_to_commit](char32_t code) {
repl.set_state(
replxx::Replxx::State(text_to_commit.c_str(), static_cast<int>(text_to_commit.size())));
return repl.invoke(Replxx::ACTION::COMMIT_LINE, code);
return repl.invoke(replxx::Replxx::ACTION::COMMIT_LINE, code);
};
}
void ReplWrapper::init_default_settings() {
void Wrapper::init_settings() {
// NOTE - a nice popular project that uses replxx
// - https://github.com/ClickHouse/ClickHouse/blob/master/base/base/ReplxxLineReader.cpp#L366
repl.set_word_break_characters(" \t");
// Setup default keybinds
// (test-play) : Ctrl-T
repl.bind_key(Replxx::KEY::control('T'), commit_text_action("(test-play)"));
// (e) : Ctrl-Q
repl.bind_key(Replxx::KEY::control('Q'), commit_text_action("(e)"));
// (lt) : Ctrl-L
repl.bind_key(Replxx::KEY::control('L'), commit_text_action("(lt)"));
/// (:stop) : Ctrl-W
repl.bind_key(Replxx::KEY::control('W'), commit_text_action("(:stop)"));
// (dbgc) : Ctrl-G
repl.bind_key(Replxx::KEY::control('G'), commit_text_action("(dbgc)"));
// (:di) : Ctrl-B
repl.bind_key(Replxx::KEY::control('B'), commit_text_action("(:di)"));
// (mi) : Ctrl-N
repl.bind_key(Replxx::KEY::control('N'), commit_text_action("(mi)"));
for (const auto& bind : repl_config.keybinds) {
char32_t code;
switch (bind.modifier) {
case KeyBind::Modifier::CTRL:
code = replxx::Replxx::KEY::control(bind.key.at(0));
break;
case KeyBind::Modifier::SHIFT:
code = replxx::Replxx::KEY::shift(bind.key.at(0));
break;
case KeyBind::Modifier::META:
code = replxx::Replxx::KEY::meta(bind.key.at(0));
break;
}
repl.bind_key(code, commit_text_action(bind.command));
}
}
// TODO - command to print out keybinds
void Wrapper::reload_startup_file() {
startup_file = load_user_startup_file(username);
}
std::string find_repl_username() {
// Two options - either:
// 1. look for the `user.txt` file, which should only contain the username
// 2. if this is absent AND there is a single folder inside the "user" folder, use that as the
// username
auto user_dir = file_util::get_jak_project_dir() / "goal_src" / "user";
auto dirs = file_util::find_directories_in_dir(user_dir);
if (dirs.size() == 1) {
return dirs.at(0).filename().string();
}
std::regex allowed_chars("(^[0-9a-zA-Z\\-\\.\\!\\?<>]*$)");
if (file_util::file_exists((user_dir / "user.txt").string())) {
auto text = file_util::read_text_file(user_dir / "user.txt");
text = str_util::trim(text);
if (!text.empty() && std::regex_match(text, allowed_chars)) {
return text;
}
}
return "#f";
}
StartupFile load_user_startup_file(const std::string& username) {
// Check for a `startup.gc` file, each line will be executed on the REPL on startup
auto startup_file_path =
file_util::get_jak_project_dir() / "goal_src" / "user" / username / "startup.gc";
StartupFile startup_file;
if (file_util::file_exists(startup_file_path.string())) {
auto data = file_util::read_text_file(startup_file_path);
auto startup_cmds = str_util::split(data);
bool found_run_on_listen_line = false;
for (const auto& cmd : startup_cmds) {
if (found_run_on_listen_line) {
startup_file.run_after_listen.push_back(cmd);
} else {
startup_file.run_before_listen.push_back(cmd);
}
if (str_util::contains(cmd, "og:run-below-on-listen")) {
found_run_on_listen_line = true;
}
}
}
return startup_file;
}
REPL::Config load_repl_config(const std::string& username, const GameVersion game_version) {
auto repl_config_path =
file_util::get_jak_project_dir() / "goal_src" / "user" / username / "repl-config.json";
if (file_util::file_exists(repl_config_path.string())) {
try {
REPL::Config config(game_version);
auto repl_config_data =
parse_commented_json(file_util::read_text_file(repl_config_path), "repl-config.json");
from_json(repl_config_data, config);
return config;
} catch (std::exception& e) {
REPL::Config config(game_version);
}
}
return REPL::Config(game_version);
}
} // namespace REPL
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <functional>
#include <optional>
#include <string>
#include <vector>
#include "config.h"
#include "third-party/replxx/include/replxx.hxx"
namespace REPL {
struct StartupFile {
std::vector<std::string> run_before_listen = {};
std::vector<std::string> run_after_listen = {};
};
class Wrapper {
replxx::Replxx repl;
public:
std::string username;
Config repl_config;
StartupFile startup_file;
std::vector<std::string> examples{};
std::vector<std::pair<std::string, replxx::Replxx::Color>> regex_colors{};
Wrapper(const std::string& _username, const Config& config, const StartupFile& startup)
: username(_username), repl_config(config), startup_file(startup) {}
replxx::Replxx& get_repl() { return repl; }
void init_settings();
void reload_startup_file();
// Functionality / Commands
void clear_screen();
void print_to_repl(const std::string& str);
void print_welcome_message();
void set_history_max_size(size_t len);
const char* readline(const std::string& prompt);
void add_to_history(const std::string& line);
void save_history();
void load_history();
void print_help_message();
void print_keybind_help();
std::pair<std::string, bool> get_current_repl_token(std::string const& context);
private:
replxx::Replxx::key_press_handler_t commit_text_action(std::string text_to_commit);
std::vector<REPL::KeyBind> keybindings = {};
};
std::string find_repl_username();
StartupFile load_user_startup_file(const std::string& username);
REPL::Config load_repl_config(const std::string& username, const GameVersion game_version);
} // namespace REPL
+10
View File
@@ -550,4 +550,14 @@ std::vector<fs::path> find_files_recursively(const fs::path& base_dir, const std
return files;
}
std::vector<fs::path> find_directories_in_dir(const fs::path& base_dir) {
std::vector<fs::path> dirs = {};
for (auto& p : fs::recursive_directory_iterator(base_dir)) {
if (p.is_directory()) {
dirs.push_back(p.path());
}
}
return dirs;
}
} // namespace file_util
+1
View File
@@ -60,4 +60,5 @@ bool dgo_header_is_compressed(const std::vector<u8>& data);
std::vector<u8> decompress_dgo(const std::vector<u8>& data_in);
FILE* open_file(const fs::path& path, const std::string& mode);
std::vector<fs::path> find_files_recursively(const fs::path& base_dir, const std::regex& pattern);
std::vector<fs::path> find_directories_in_dir(const fs::path& base_dir);
} // namespace file_util
+3
View File
@@ -296,6 +296,8 @@ std::vector<std::string> SplitString(const ::std::string& str, char delimiter =
}
} // namespace
namespace google_diff {
std::string diff_strings(const std::string& lhs, const std::string& rhs) {
if (!lhs.empty() && !rhs.empty()) {
const std::vector<std::string> lhs_lines = SplitString(lhs);
@@ -310,3 +312,4 @@ std::string diff_strings(const std::string& lhs, const std::string& rhs) {
std::vector<std::string> split_string(const ::std::string& str, char delimiter) {
return SplitString(str, delimiter);
}
} // namespace google_diff
+2 -1
View File
@@ -6,6 +6,7 @@
/*!
* Diff two strings. This uses the code from gtest's diff implementation.
*/
namespace google_diff {
std::string diff_strings(const std::string& lhs, const std::string& rhs);
std::vector<std::string> split_string(const ::std::string& str, char delimiter = '\n');
} // namespace google_diff
+10
View File
@@ -2,6 +2,8 @@
#include <regex>
#include "common/util/diff.h"
namespace str_util {
const std::string WHITESPACE = " \n\r\t\f\v";
@@ -47,4 +49,12 @@ bool valid_regex(const std::string& regex) {
}
return true;
}
std::string diff(const std::string& lhs, const std::string& rhs) {
return google_diff::diff_strings(lhs, rhs);
}
/// Default splits on \n characters
std::vector<std::string> split(const ::std::string& str, char delimiter) {
return google_diff::split_string(str, delimiter);
}
} // namespace str_util
+6
View File
@@ -1,4 +1,7 @@
#pragma once
#include <string>
#include <vector>
namespace str_util {
bool contains(const std::string& s, const std::string& substr);
@@ -8,4 +11,7 @@ std::string rtrim(const std::string& s);
std::string trim(const std::string& s);
int line_count(const std::string& str);
bool valid_regex(const std::string& regex);
std::string diff(const std::string& lhs, const std::string& rhs);
/// Default splits on \n characters
std::vector<std::string> split(const ::std::string& str, char delimiter = '\n');
} // namespace str_util
+4
View File
@@ -29,3 +29,7 @@ std::string version_to_game_name(GameVersion v) {
ASSERT_MSG(false, fmt::format("no game_name for version: {} found", fmt::underlying(v)));
}
}
std::vector<std::string> valid_game_version_names() {
return {game_version_names[GameVersion::Jak1], game_version_names[GameVersion::Jak2]};
}
+2
View File
@@ -6,6 +6,7 @@
*/
#include <string>
#include <vector>
#include "common/common_types.h"
@@ -55,3 +56,4 @@ constexpr PerGameVersion<const char*> game_version_names = {"jak1", "jak2"};
GameVersion game_name_to_version(const std::string& name);
bool valid_game_version(const std::string& name);
std::string version_to_game_name(GameVersion v);
std::vector<std::string> valid_game_version_names();
+1 -1
View File
@@ -3,7 +3,7 @@
#include <optional>
#include <string_view>
#include "common/nrepl/ReplClient.h"
#include "common/repl/nrepl/ReplClient.h"
#include "common/serialization/subtitles/subtitles_ser.h"
#include "third-party/imgui/imgui.h"
+57 -2
View File
@@ -5,6 +5,8 @@ e.g. for username `mark` make a directory called `mark`
Inside that directory, create `user.gs` and `user.gc` files.
These are your own user scripts, loaded after the GOOS library and GOAL library respectively.
> Alternatively, if you only have a single folder in `goal_src/user/` it will be assumed to be your user folder
The rest of the directory can be used however you please!
To automatically log in as a specific user, create a `user.txt` file in this directory
@@ -13,7 +15,60 @@ modify multiple scripts when you want to change users.
If you want to make your profile public, edit the .gitignore in this directory.
Additionally, you can provide a `repl-config.json` to set various REPL settings:
- `numConnectToTargetAttempts` - the number of times the REPL will attempt to connect to the target on an `(lt)`
Additionally, you can provide a `repl-config.json` to set various REPL settings, an example configuration:
```json
{
"numConnectToTargetAttempts": 1,
"jak1": {
"asmFileSearchDirs": [
"goal_src/jak1"
]
},
"jak2": {
"asmFileSearchDirs": [
"goal_src/jak2"
]
},
"appendKeybinds": true,
"keybinds": [
{
"modifier": "ctrl",
"key": "S",
"description": "Test Bind",
"command": "(format 0 \"hello world\")"
}
]
}
```
And a `startup.gc` where each line will be executed upon startup
## Re-running certain commands upon listening to the target
A common workflow that you might want in your `startup.gc` is something like the following:
```clj
(mi)
(lt)
(dbgc)
(test-play)
```
This builds the game, connects to the game, attaches the debugger, and runs it.
However, when you crash you ideally want to just be able to:
- stop the game via `(:stop)` or the respective keybind
- fix the code, rebuild just that file
- re-launch the game and re-connect
Upon which you'd probably want to run all or some of the above startup again. But how can you accomplish this without re-launching the REPL completely? Like so:
```clj
(mi)
(lt)
;; og:run-below-on-listen
(dbgc)
(test-play)
```
As the comment suggests, upon a succesful `(lt)` it will run any lines below it again.
+14 -11
View File
@@ -20,7 +20,7 @@ using namespace goos;
Compiler::Compiler(GameVersion version,
const std::string& user_profile,
std::unique_ptr<ReplWrapper> repl)
std::unique_ptr<REPL::Wrapper> repl)
: m_version(version),
m_goos(user_profile),
m_debugger(&m_listener, &m_goos.reader, version),
@@ -63,9 +63,9 @@ Compiler::Compiler(GameVersion version,
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_default_settings();
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)));
@@ -397,7 +397,7 @@ void Compiler::asm_file(const CompilationOptions& options) {
if (file_path.empty()) {
lg::print("ERROR - can't load a file without a providing a path\n");
return;
} else if (m_asm_file_search_dirs.empty()) {
} 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);
@@ -406,14 +406,17 @@ void Compiler::asm_file(const CompilationOptions& options) {
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 = {};
for (const auto& dir : m_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 (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;
+12 -17
View File
@@ -5,7 +5,7 @@
#include <optional>
#include "common/goos/Interpreter.h"
#include "common/goos/ReplUtils.h"
#include "common/repl/util.h"
#include "common/type_system/TypeSystem.h"
#include "goalc/compiler/CompilerException.h"
@@ -41,12 +41,12 @@ class Compiler {
public:
Compiler(GameVersion version,
const std::string& user_profile = "#f",
std::unique_ptr<ReplWrapper> repl = nullptr);
std::unique_ptr<REPL::Wrapper> repl = nullptr);
~Compiler();
void asm_file(const CompilationOptions& options);
void save_repl_history();
void print_to_repl(const std::string_view& str);
void print_to_repl(const std::string& str);
std::string get_prompt();
std::string get_repl_input();
ReplStatus handle_repl_string(const std::string& input);
@@ -81,21 +81,19 @@ class Compiler {
listener::Listener& listener() { return m_listener; }
void poke_target() { m_listener.send_poke(); }
bool connect_to_target();
Replxx::completions_t find_symbols_or_object_file_by_prefix(
replxx::Replxx::completions_t find_symbols_or_object_file_by_prefix(
std::string const& context,
int& contextLen,
std::vector<std::string> const& user_data);
Replxx::hints_t find_hints_by_prefix(std::string const& context,
int& contextLen,
Replxx::Color& color,
std::vector<std::string> const& user_data);
replxx::Replxx::hints_t find_hints_by_prefix(std::string const& context,
int& contextLen,
replxx::Replxx::Color& color,
std::vector<std::string> const& user_data);
void repl_coloring(std::string const& str,
Replxx::colors_t& colors,
std::vector<std::pair<std::string, Replxx::Color>> const& user_data);
replxx::Replxx::colors_t& colors,
std::vector<std::pair<std::string, replxx::Replxx::Color>> const& user_data);
bool knows_object_file(const std::string& name);
MakeSystem& make_system() { return m_make; }
void update_via_config_file(const std::string& json,
const std::optional<std::string> game_name = {});
private:
GameVersion m_version;
@@ -114,13 +112,9 @@ class Compiler {
bool m_throw_on_define_extern_redefinition = false;
std::unordered_set<std::string> m_allow_inconsistent_definition_symbols;
SymbolInfoMap m_symbol_info;
std::unique_ptr<ReplWrapper> m_repl;
std::unique_ptr<REPL::Wrapper> m_repl;
MakeSystem m_make;
// Configurable fields
int m_target_connect_attempts = 30;
std::vector<std::string> m_asm_file_search_dirs = {};
struct DebugStats {
int num_spills = 0;
int num_spills_v1 = 0;
@@ -602,6 +596,7 @@ class Compiler {
Val* compile_asm_data_file(const goos::Object& form, const goos::Object& rest, Env* env);
Val* compile_asm_text_file(const goos::Object& form, const goos::Object& rest, Env* env);
Val* compile_repl_help(const goos::Object& form, const goos::Object& rest, Env* env);
Val* compile_repl_keybinds(const goos::Object& form, const goos::Object& rest, Env* env);
Val* compile_listen_to_target(const goos::Object& form, const goos::Object& rest, Env* env);
Val* compile_reset_target(const goos::Object& form, const goos::Object& rest, Env* env);
Val* compile_poke(const goos::Object& form, const goos::Object& rest, Env* env);
+1 -17
View File
@@ -9,7 +9,7 @@ void Compiler::save_repl_history() {
m_repl->save_history();
}
void Compiler::print_to_repl(const std::string_view& str) {
void Compiler::print_to_repl(const std::string& str) {
m_repl->print_to_repl(str);
}
@@ -154,22 +154,6 @@ bool Compiler::knows_object_file(const std::string& name) {
return m_debugger.knows_object(name);
}
void Compiler::update_via_config_file(const std::string& json,
const std::optional<std::string> game_name) {
auto cfg = parse_commented_json(json, "repl-config.json");
if (cfg.contains("numConnectToTargetAttempts")) {
m_target_connect_attempts = cfg.at("numConnectToTargetAttempts").get<int>();
}
if (cfg.contains("asmFileSearchDirs")) {
m_asm_file_search_dirs = cfg.at("asmFileSearchDirs").get<std::vector<std::string>>();
}
// If there are any game specific config entries, set or override with them
if (game_name && cfg.contains(game_name.value())) {
auto game_cfg = cfg.at(game_name.value()).get<nlohmann::json>();
update_via_config_file(game_cfg.dump(), {});
}
}
/*!
* Parse arguments into a goos::Arguments format.
*/
+1
View File
@@ -140,6 +140,7 @@ const std::unordered_map<
// COMPILER CONTROL
{"repl-help", &Compiler::compile_repl_help},
{"repl-keybinds", &Compiler::compile_repl_keybinds},
{":clear", &Compiler::compile_repl_clear_screen},
{"gs", &Compiler::compile_gs},
{":exit", &Compiler::compile_exit},
+31 -13
View File
@@ -6,7 +6,7 @@
#include <regex>
#include <stack>
#include "common/goos/ReplUtils.h"
#include "common/repl/util.h"
#include "common/util/DgoWriter.h"
#include "common/util/FileUtil.h"
#include "common/util/Timer.h"
@@ -177,6 +177,14 @@ Val* Compiler::compile_repl_help(const goos::Object&, const goos::Object&, Env*)
return get_none();
}
/*!
* Print out all set keybinds for the REPL (by our tooling)
*/
Val* Compiler::compile_repl_keybinds(const goos::Object&, const goos::Object&, Env*) {
m_repl.get()->print_keybind_help();
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
@@ -208,7 +216,17 @@ Val* Compiler::compile_listen_to_target(const goos::Object& form,
}
});
m_listener.connect_to_target(m_target_connect_attempts, ip, port);
int retries = 30;
if (m_repl) {
retries = m_repl->repl_config.target_connect_attempts;
}
auto connected = m_listener.connect_to_target(retries, ip, port);
if (connected && m_repl) {
m_repl->reload_startup_file();
for (const auto& line : m_repl->startup_file.run_after_listen) {
handle_repl_string(line);
}
}
return get_none();
}
@@ -379,13 +397,13 @@ Val* Compiler::compile_get_info(const goos::Object& form, const goos::Object& re
return get_none();
}
Replxx::completions_t Compiler::find_symbols_or_object_file_by_prefix(
replxx::Replxx::completions_t Compiler::find_symbols_or_object_file_by_prefix(
std::string const& context,
int& contextLen,
std::vector<std::string> const& user_data) {
(void)contextLen;
(void)user_data;
Replxx::completions_t completions;
replxx::Replxx::completions_t completions;
// If we are trying to execute a `(ml ...)` we can automatically get the object file
// insert quotes if needed as well.
@@ -418,16 +436,16 @@ Replxx::completions_t Compiler::find_symbols_or_object_file_by_prefix(
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) {
replxx::Replxx::hints_t Compiler::find_hints_by_prefix(std::string const& context,
int& contextLen,
replxx::Replxx::Color& color,
std::vector<std::string> const& user_data) {
(void)contextLen;
(void)user_data;
auto token = m_repl->get_current_repl_token(context);
auto possible_forms = lookup_symbol_infos_starting_with(token.first);
Replxx::hints_t hints;
replxx::Replxx::hints_t hints;
// TODO - hints for `(ml ...` as well
@@ -440,7 +458,7 @@ Replxx::hints_t Compiler::find_hints_by_prefix(std::string const& context,
// set hint color to green if single match found
if (hints.size() == 1) {
color = Replxx::Color::GREEN;
color = replxx::Replxx::Color::GREEN;
}
return hints;
@@ -448,10 +466,10 @@ Replxx::hints_t Compiler::find_hints_by_prefix(std::string const& context,
void Compiler::repl_coloring(
std::string const& context,
Replxx::colors_t& colors,
std::vector<std::pair<std::string, Replxx::Color>> const& regex_color) {
replxx::Replxx::colors_t& colors,
std::vector<std::pair<std::string, replxx::Replxx::Color>> const& regex_color) {
(void)regex_color;
using cl = Replxx::Color;
using cl = replxx::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;
+69 -92
View File
@@ -1,11 +1,12 @@
#include <cstdio>
#include <regex>
#include "common/goos/ReplUtils.h"
#include "common/log/log.h"
#include "common/nrepl/ReplServer.h"
#include "common/repl/nrepl/ReplServer.h"
#include "common/repl/util.h"
#include "common/util/FileUtil.h"
#include "common/util/diff.h"
#include "common/util/string_util.h"
#include "common/versions.h"
#include "goalc/compiler/Compiler.h"
@@ -27,7 +28,6 @@ int main(int argc, char** argv) {
bool auto_debug = false;
bool auto_find_user = false;
std::string cmd = "";
std::string startup_cmd = "";
std::string username = "#f";
std::string game = "jak1";
int nrepl_port = 8181;
@@ -36,9 +36,7 @@ int main(int argc, char** argv) {
// TODO - a lot of these flags could be deprecated and moved into `repl-config.json`
// TODO - auto-find the user if there is only one folder within `user/`
CLI::App app{"OpenGOAL Compiler / REPL"};
app.add_option("-c,--cmd", cmd, "Specify a command to run");
app.add_option("--startup-cmd", startup_cmd,
"Specify a command to run and keep the REPL open afterwards");
app.add_option("-c,--cmd", cmd, "Specify a command to run, no REPL is launched in this mode");
app.add_option("-u,--user", username,
"Specify the username to use for your user profile in 'goal_src/user/'");
app.add_option("-p,--port", nrepl_port, "Specify the nREPL port. Defaults to 8181");
@@ -47,13 +45,25 @@ int main(int argc, char** argv) {
app.add_flag("--auto-dbg", auto_debug,
"Attempt to automatically connect to the debugger on startup");
app.add_flag("--user-auto", auto_find_user,
"Attempt to automatically deduce the user, overrides '-user'");
"Attempt to automatically deduce the user, overrides '--user'");
app.add_option("-g,--game", game, "The game name: 'jak1' or 'jak2'");
app.add_option("--proj-path", project_path_override,
"Specify the location of the 'data/' folder");
app.validate_positionals();
CLI11_PARSE(app, argc, argv);
// Yell about deprecations
if (auto_listen) {
lg::warn(
"--auto-lt will be deprecated, migrate to a 'startup.gc' file in your goal_src/user "
"folder");
}
if (auto_debug) {
lg::warn(
"--auto-dbg will be deprecated, migrate to a 'startup.gc' file in your goal_src/user "
"folder");
}
GameVersion game_version = game_name_to_version(game);
if (!project_path_override.empty()) {
@@ -69,74 +79,60 @@ int main(int argc, char** argv) {
return 1;
}
std::vector<std::string> user_startup_commands = {};
std::optional<std::string> repl_config = {};
if (auto_find_user) {
username = "#f";
std::regex allowed_chars("[0-9a-zA-Z\\-\\.\\!\\?<>]");
try {
auto text = std::make_shared<goos::FileText>(
file_util::get_file_path({"goal_src", "user", "user.txt"}), "goal_src/user/user.txt");
goos::TextStream ts(text);
ts.seek_past_whitespace_and_comments();
std::string found_username;
while (ts.text_remains()) {
auto character = ts.read();
if (!std::regex_match(std::string(1, character), allowed_chars)) {
break;
}
found_username.push_back(character);
}
if (!found_username.empty()) {
username = found_username;
// Check for a `startup.gc` file, each line will be executed on the REPL on startup
auto startup_file_path =
file_util::get_file_path({"goal_src", "user", username, "startup.gc"});
if (file_util::file_exists(startup_file_path)) {
auto data = file_util::read_text_file(startup_file_path);
auto startup_cmds = split_string(data);
for (const auto& cmd : startup_cmds) {
user_startup_commands.push_back(cmd);
}
}
// Check for a `repl-config.json` file, so things can be configured without tons of flags
auto repl_config_path =
file_util::get_file_path({"goal_src", "user", username, "repl-config.json"});
if (file_util::file_exists(repl_config_path)) {
repl_config = file_util::read_text_file(repl_config_path);
}
}
} catch (std::exception& e) {
printf("error opening user desc file: %s\n", e.what());
}
}
setup_logging();
lg::info("OpenGOAL Compiler {}.{}", versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR);
// Init REPL
// Figure out the username
if (auto_find_user) {
username = REPL::find_repl_username();
}
// Load the user's startup file
auto startup_file = REPL::load_user_startup_file(username);
// TODO - deprecate these two flags
if (startup_file.run_before_listen.empty() && (auto_debug || auto_listen)) {
startup_file.run_before_listen.push_back("(lt)");
}
if (startup_file.run_after_listen.empty() && (auto_debug || auto_listen)) {
startup_file.run_before_listen.push_back("(dbgc)");
}
// Load the user's REPL config
auto repl_config = REPL::load_repl_config(username, game_version);
// Init Compiler
std::unique_ptr<Compiler> compiler;
std::mutex compiler_mutex;
// if a command is provided on the command line, no REPL just run the compiler on it
try {
if (!cmd.empty()) {
compiler = std::make_unique<Compiler>(game_version);
compiler->run_front_end_on_string(cmd);
return 0;
}
} catch (std::exception& e) {
lg::error("Compiler Fatal Error: {}", e.what());
}
// Otherwise, start the REPL normally
ReplStatus status = ReplStatus::OK;
std::function<void()> repl_startup_func = [&]() {
// Run automatic forms if applicable
std::lock_guard<std::mutex> lock(compiler_mutex);
for (const auto& cmd : startup_file.run_before_listen) {
status = compiler->handle_repl_string(cmd);
}
};
// Initialize nREPL server socket
std::function<bool()> shutdown_callback = [&]() { return status == ReplStatus::WANT_EXIT; };
ReplServer repl_server(shutdown_callback, nrepl_port);
bool repl_server_ok = repl_server.init_server();
std::thread nrepl_thread;
// the compiler may throw an exception if it fails to load its standard library.
try {
std::unique_ptr<Compiler> compiler;
std::mutex compiler_mutex;
// if a command is provided on the command line, no REPL just run the compiler on it
if (!cmd.empty()) {
compiler = std::make_unique<Compiler>(game_version);
compiler->run_front_end_on_string(cmd);
return 0;
}
compiler = std::make_unique<Compiler>(game_version, username, std::make_unique<ReplWrapper>());
if (repl_config) {
compiler->update_via_config_file(repl_config.value(), game);
}
// Start nREPL Server
compiler = std::make_unique<Compiler>(
game_version, username,
std::make_unique<REPL::Wrapper>(username, repl_config, startup_file));
// Start nREPL Server if it spun up successfully
if (repl_server_ok) {
nrepl_thread = std::thread([&]() {
while (!shutdown_callback()) {
@@ -151,23 +147,7 @@ int main(int argc, char** argv) {
}
});
}
// Run automatic forms if applicable
// - this should probably be deprecated in favor of the `startup.gc` file
if (user_startup_commands.empty() && (auto_debug || auto_listen)) {
user_startup_commands.push_back("(lt)");
}
if (user_startup_commands.empty() && auto_debug) {
std::lock_guard<std::mutex> lock(compiler_mutex);
status = compiler->handle_repl_string("(dbgc)");
user_startup_commands.push_back("(dbgc)");
}
if (!user_startup_commands.empty()) {
std::lock_guard<std::mutex> lock(compiler_mutex);
for (const auto& cmd : user_startup_commands) {
status = compiler->handle_repl_string(cmd);
}
}
repl_startup_func();
// Poll Terminal
while (status != ReplStatus::WANT_EXIT) {
@@ -177,18 +157,13 @@ int main(int argc, char** argv) {
if (compiler) {
compiler->save_repl_history();
}
compiler =
std::make_unique<Compiler>(game_version, username, std::make_unique<ReplWrapper>());
if (repl_config) {
compiler->update_via_config_file(repl_config.value(), game);
}
if (!startup_cmd.empty()) {
compiler->handle_repl_string(startup_cmd);
// reset to prevent re-executing on manual reload
startup_cmd = "";
}
compiler = std::make_unique<Compiler>(
game_version, username,
std::make_unique<REPL::Wrapper>(username, repl_config, startup_file));
status = ReplStatus::OK;
repl_startup_func();
}
// process user input
std::string input_from_stdin = compiler->get_repl_input();
if (!input_from_stdin.empty()) {
// lock, while we compile
@@ -201,6 +176,8 @@ int main(int argc, char** argv) {
status = ReplStatus::WANT_EXIT;
}
// TODO - investigate why there is such a delay when exitting
// Cleanup
if (repl_server_ok) {
repl_server.shutdown_server();
+2 -3
View File
@@ -1,6 +1,5 @@
#include "execution.h"
#include "common/util/diff.h"
#include "common/util/string_util.h"
#include "goalc/compiler/Compiler.h"
@@ -36,7 +35,7 @@ void decompile(OfflineTestDecompiler& dc,
std::string clean_decompilation_code(const std::string& in, const bool leave_comments = false) {
std::string out = in;
if (!leave_comments) {
std::vector<std::string> lines = split_string(in);
std::vector<std::string> lines = str_util::split(in);
// Remove all lines that are comments
// comments are added only by us, meaning this _should_ be consistent
std::vector<std::string>::iterator line_itr = lines.begin();
@@ -82,7 +81,7 @@ OfflineTestCompareResult compare(OfflineTestDecompiler& dc,
compare_result.total_files++;
compare_result.total_lines += str_util::line_count(result);
if (result != ref) {
compare_result.failing_files.push_back({file.unique_name, diff_strings(ref, result)});
compare_result.failing_files.push_back({file.unique_name, str_util::diff(ref, result)});
compare_result.total_pass = false;
if (config.dump_mode) {
auto failure_dir = file_util::get_jak_project_dir() / "failures";
+2 -2
View File
@@ -311,11 +311,11 @@ void OfflineTestThreadManager::print_current_test_status(const OfflineTestConfig
fmt::print("\x1b[{}A", lines_to_clear); // move n lines up
fmt::print("\e[?25l"); // hide the cursor
int threads_shown = 0;
for (int i = 0; i < statuses.size() && threads_shown < threads_to_display; i++) {
for (int i = 0; i < (int)statuses.size() && threads_shown < threads_to_display; i++) {
const auto& status = statuses.at(i);
// Skip completed threads if there are potential in-progress ones to show
if (threads_hidden != 0 && !status->in_progress() &&
(statuses.size() - i) > threads_to_display) {
((int)statuses.size() - i) > threads_to_display) {
continue;
}