Skip to content

Commit

Permalink
#144 Add interactive command line debugger support
Browse files Browse the repository at this point in the history
WIP
  • Loading branch information
cmannett85 committed Feb 3, 2021
1 parent a3216af commit 1300a04
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 20 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ set(HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/include/malbolge/utility/from_chars.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/malbolge/utility/raii.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/malbolge/utility/signal.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/malbolge/utility/stream_helpers.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/malbolge/utility/string_constant.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/malbolge/utility/string_view_ops.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/malbolge/utility/tuple_iterator.hpp
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ A coverage report of the latest master commit is available [here](https://cmanne
* Boost v1.71
* CMake v3.12
* Emscripten v2.0.10 (only needed for WASM build)
* Doxygen (only needed for Documentation build)
* Doxygen v1.8.18 (only needed for Documentation build)

---

Expand Down
14 changes: 13 additions & 1 deletion cmake/build_types/standard_executable.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
# See LICENSE file
#

add_executable(malbolge ${MAIN_SRCS})
set(CURSES_NEED_NCURSES ON)
find_package(Curses REQUIRED)

set(TERMINAL_GUI_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/include/malbolge/ui/terminal.hpp
)

set(TERMINAL_GUI_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/src/ui/terminal.cpp
)

add_executable(malbolge ${MAIN_SRCS} ${TERMINAL_GUI_HEADERS} ${TERMINAL_GUI_SRCS})

# The documentation is built so we can add the API version of the README to the
# installation packages, this is needed as the 'base' README.md has external
Expand All @@ -28,6 +39,7 @@ target_include_directories(malbolge
target_link_libraries(malbolge
PUBLIC Threads::Threads
PUBLIC malbolge_lib
PUBLIC ${CURSES_LIBRARIES}
)

install(TARGETS malbolge
Expand Down
1 change: 1 addition & 0 deletions cmake/build_types/unit_test.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ set(TEST_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/utility/from_chars_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/utility/raii_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/utility/signal_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/utility/stream_helpers_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/utility/string_constant_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/utility/string_view_ops_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/utility/tuple_iterator_test.cpp
Expand Down
53 changes: 53 additions & 0 deletions include/malbolge/ui/terminal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* Cam Mannett 2021
*
* See LICENSE file
*/

#pragma once

#include "malbolge/virtual_memory.hpp"

namespace malbolge
{
class argument_parser;

/** Namespace for all GUI types and functions.
*/
namespace ui
{
/** Represents an interactive terminal GUI, based on ncurses.
*
* This class can be moved, but not copied.
*/
class terminal
{
public:
/** Constructor.
*
* @param arg_parser Arguments from the command line to influence initial
* behaviour
* @param vmem Preloaded virtual memory, if it has been specified on the
* commandline
*/
explicit terminal(const argument_parser& arg_parser,
std::optional<virtual_memory> vmem = {});

terminal(const terminal&) = delete;
terminal& operator=(const terminal&) = delete;
terminal(terminal&&) noexcept = default;
terminal& operator=(terminal&&) noexcept = delete;

/** Runs the interactive terminal UI.
*
* This function blocks until the UI is exited, which should signal the
* application to exit.
* @exception basic_exception Thrown if called on a moved-from instance
*/
void run();

private:
struct impl_t;
std::shared_ptr<impl_t> impl_;
};
}
}
21 changes: 18 additions & 3 deletions include/malbolge/utility/argument_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ class argument_parser
return force_nn_;
}

/** Enable interactive mode.
*
* This instructs the executable to go into an interactive terminal session
* driven by ncurses.
* @return True to enter interactive mode
*/
[[nodiscard]]
bool interactive_mode() const noexcept
{
return interactive_;
}

/** Returns the debugger script path, or an empty optional if not specified.
*
* @return Debugger script path, if specified
Expand All @@ -119,12 +131,15 @@ class argument_parser
}

private:
bool help_;
bool version_;
program_data p_;
log::level log_level_;
bool force_nn_;
std::optional<std::filesystem::path> debugger_script_;

// Flags
std::uint8_t help_ : 1;
std::uint8_t version_ : 1;
std::uint8_t force_nn_ : 1;
std::uint8_t interactive_ : 1;
};

/** Prints the program source into @a stream.
Expand Down
36 changes: 36 additions & 0 deletions include/malbolge/utility/stream_helpers.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* Cam Mannett 2021
*
* See LICENSE file
*/

#pragma once

#include <istream>

namespace malbolge
{
namespace utility
{
/** Returns true if there is data available to read on @a stream.
*
* As expected, this will not change the current read position of stream.
*
* @note This will return false if <TT>!stream</TT> is true.
* @tparam CharT Character type of stream
* @tparam Traits Character trait type
* @param stream Input stream to test
* @return True if there is buffered data
*/
template <typename CharT, typename Traits = std::char_traits<CharT>>
bool data_available(std::basic_istream<CharT, Traits>& stream)
{
const auto orig_pos = stream.tellg();
stream.seekg(0, stream.end);

const auto buf_size = stream.tellg() - orig_pos;
stream.seekg(orig_pos, stream.beg);

return buf_size > 0;
}
}
}
27 changes: 22 additions & 5 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
#include "malbolge/loader.hpp"
#include "malbolge/version.hpp"
#include "malbolge/utility/argument_parser.hpp"
#include "malbolge/utility/stream_helpers.hpp"
#include "malbolge/debugger/script_parser.hpp"
#include "malbolge/ui/terminal.hpp"

#include <boost/asio/io_context.hpp>
#include <boost/asio/executor_work_guard.hpp>
Expand All @@ -27,7 +29,7 @@ namespace
constexpr auto dbgr_colour = log::colour::BLUE;

[[nodiscard]]
virtual_memory load_program(argument_parser& parser)
std::optional<virtual_memory> load_program(argument_parser& parser)
{
auto mode = load_normalised_mode::AUTO;
if (parser.force_non_normalised()) {
Expand All @@ -42,7 +44,12 @@ virtual_memory load_program(argument_parser& parser)
// Load from passed in string data
return load(std::move(program.data), mode);
} else {
// Load from stdin
// Load from stdin. If we are in interactive mode, it is OK to start
// without loading anything
if (parser.interactive_mode() && !utility::data_available(std::cin)) {
return {};
}

return load_from_cin(mode);
}
}
Expand Down Expand Up @@ -97,6 +104,12 @@ void run_script_runner(const std::filesystem::path& path, virtual_memory vmem)
runner.run(std::move(vmem), seq);
}

void run_interactive(const argument_parser& parser,
std::optional<virtual_memory> vmem)
{
ui::terminal{parser, std::move(vmem)}.run();
}

void run_program(virtual_memory vmem)
{
auto ctx = boost::asio::io_context{};
Expand Down Expand Up @@ -137,13 +150,17 @@ void run_program(virtual_memory vmem)
ctx.run();
}

void run(argument_parser& parser, virtual_memory vmem)
void run(argument_parser& parser, std::optional<virtual_memory> vmem)
{
// It's OK to dereference the non-interactive vmems because it can only be
// empty in interactive mode
auto script_path = parser.debugger_script();
if (script_path) {
run_script_runner(*script_path, std::move(vmem));
run_script_runner(*script_path, std::move(*vmem));
} else if (parser.interactive_mode()) {
run_interactive(parser, std::move(vmem));
} else {
run_program(std::move(vmem));
run_program(std::move(*vmem));
}
}
}
Expand Down
46 changes: 46 additions & 0 deletions src/ui/terminal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* Cam Mannett 2021
*
* See LICENSE file
*/

#include "malbolge/ui/terminal.hpp"
#include "malbolge/utility/argument_parser.hpp"
#include "malbolge/log.hpp"

#include <boost/asio/io_context.hpp>
#include <boost/asio/executor_work_guard.hpp>

#include <ncurses.h>

using namespace malbolge;

struct ui::terminal::impl_t
{
explicit impl_t(const argument_parser& arg_parser,
std::optional<virtual_memory> vmem) :
pdata{arg_parser.program()},
force_nn{arg_parser.force_non_normalised()},
vmem(std::move(vmem))
{}

argument_parser::program_data pdata;
bool force_nn;
boost::asio::io_context ctx;

std::optional<virtual_memory> vmem;
};

ui::terminal::terminal(const argument_parser& arg_parser,
std::optional<virtual_memory> vmem) :
impl_{std::make_shared<impl_t>(arg_parser, std::move(vmem))}
{}

void ui::terminal::run()
{
log::print(log::INFO, "Here");

initscr();
printw("Hello world");
refresh();
endwin();
}
28 changes: 24 additions & 4 deletions src/utility/argument_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ constexpr auto log_flag_prefix = "-l";
constexpr auto string_flag = "--string";
constexpr auto debugger_script_flag = "--debugger-script";
constexpr auto force_nn_flag = "--force-non-normalised";
constexpr auto interactive_flag = "-i";
}

argument_parser::argument_parser(int argc, char* argv[]) :
help_{false},
version_{false},
p_{program_source::STDIN, ""},
log_level_{log::ERROR},
force_nn_{false}
help_{false},
version_{false},
force_nn_{false},
interactive_{false}
{
// Convert to string_views, they're easier to work with
auto args = std::deque<std::string_view>(argc-1);
Expand Down Expand Up @@ -66,6 +68,7 @@ argument_parser::argument_parser(int argc, char* argv[]) :
args.erase(force_nn_it);
}

// Load from string
auto string_it = std::find(args.begin(), args.end(), string_flag);
if (string_it != args.end()) {
// Move the iterator forward one to extract the program data
Expand All @@ -83,6 +86,7 @@ argument_parser::argument_parser(int argc, char* argv[]) :
args.erase(string_it);
}

// Debugger script path
auto debugger_script_it = std::find(args.begin(), args.end(), debugger_script_flag);
if (debugger_script_it != args.end()) {
// Move the iterator forward one to extract the script path
Expand All @@ -99,6 +103,20 @@ argument_parser::argument_parser(int argc, char* argv[]) :
args.erase(debugger_script_it);
}

// Interactive mode
auto i_mode_it = std::find(args.begin(), args.end(), interactive_flag);
if (i_mode_it != args.end()) {
if (debugger_script_) {
throw system_exception{
"Cannot use interactive mode with a debugger script specified",
std::errc::invalid_argument
};
}

interactive_ = true;
args.erase(i_mode_it);
}

// Log level
if (args.size() && args.front().starts_with(log_flag_prefix)) {
// There must only be 'l's
Expand Down Expand Up @@ -185,5 +203,7 @@ std::ostream& malbolge::operator<<(std::ostream& stream, const argument_parser&)
<< "\t" << debugger_script_flag
<< "\tRun the given debugger script on the program\n"
<< "\t" << force_nn_flag
<< "\tOverride normalised program detection to force to non-normalised";
<< "\tOverride normalised program detection to force to non-normalised\n"
<< "\t" << interactive_flag
<< "\t\t\tEnter an interactive terminal session";
}
10 changes: 5 additions & 5 deletions src/virtual_cpu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class virtual_cpu::impl_t : public std::enable_shared_from_this<impl_t>
std::thread thread;

virtual_memory vmem;
std::deque<input> input_queue_;
std::deque<input> input_queue;
std::unordered_map<math::ternary, breakpoint> bps;

// vCPU Registers
Expand Down Expand Up @@ -221,7 +221,7 @@ void virtual_cpu::add_input(std::string data)
{
impl_check();
boost::asio::post(impl_->ctx, [impl = impl_, data = std::move(data)]() {
impl->input_queue_.emplace_back(std::move(data));
impl->input_queue.emplace_back(std::move(data));
if (impl->state() == execution_state::WAITING_FOR_INPUT) {
impl->set_state(execution_state::RUNNING);
impl->run();
Expand Down Expand Up @@ -372,16 +372,16 @@ void virtual_cpu::impl_t::run(bool schedule_next)
break;
case cpu_instruction::read:
{
if (input_queue_.empty()) {
if (input_queue.empty()) {
set_state(virtual_cpu::execution_state::WAITING_FOR_INPUT);
log::print(log::VERBOSE_DEBUG, "\tWaiting for input...");
return;
}

auto c = input_queue_.front().get();
auto c = input_queue.front().get();
if (!c) {
a = math::ternary::max;
input_queue_.pop_front();
input_queue.pop_front();
} else {
a = c;
}
Expand Down
Loading

0 comments on commit 1300a04

Please sign in to comment.