diff --git a/CMakeLists.txt b/CMakeLists.txt index dd1cade..fb1d189 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index 7e07330..d8609e0 100644 --- a/README.md +++ b/README.md @@ -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) --- diff --git a/cmake/build_types/standard_executable.cmake b/cmake/build_types/standard_executable.cmake index 6fa82b7..4a8305b 100644 --- a/cmake/build_types/standard_executable.cmake +++ b/cmake/build_types/standard_executable.cmake @@ -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 @@ -28,6 +39,7 @@ target_include_directories(malbolge target_link_libraries(malbolge PUBLIC Threads::Threads PUBLIC malbolge_lib + PUBLIC ${CURSES_LIBRARIES} ) install(TARGETS malbolge diff --git a/cmake/build_types/unit_test.cmake b/cmake/build_types/unit_test.cmake index e097f67..da002e2 100644 --- a/cmake/build_types/unit_test.cmake +++ b/cmake/build_types/unit_test.cmake @@ -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 diff --git a/include/malbolge/ui/terminal.hpp b/include/malbolge/ui/terminal.hpp new file mode 100644 index 0000000..a271b18 --- /dev/null +++ b/include/malbolge/ui/terminal.hpp @@ -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 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_; +}; +} +} diff --git a/include/malbolge/utility/argument_parser.hpp b/include/malbolge/utility/argument_parser.hpp index 3a8160d..dd4109a 100644 --- a/include/malbolge/utility/argument_parser.hpp +++ b/include/malbolge/utility/argument_parser.hpp @@ -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 @@ -119,12 +131,15 @@ class argument_parser } private: - bool help_; - bool version_; program_data p_; log::level log_level_; - bool force_nn_; std::optional 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. diff --git a/include/malbolge/utility/stream_helpers.hpp b/include/malbolge/utility/stream_helpers.hpp new file mode 100644 index 0000000..ac190be --- /dev/null +++ b/include/malbolge/utility/stream_helpers.hpp @@ -0,0 +1,36 @@ +/* Cam Mannett 2021 + * + * See LICENSE file + */ + +#pragma once + +#include + +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 !stream 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 > +bool data_available(std::basic_istream& 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; +} +} +} diff --git a/src/main.cpp b/src/main.cpp index c0c5c61..4da3c8d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 #include @@ -27,7 +29,7 @@ namespace constexpr auto dbgr_colour = log::colour::BLUE; [[nodiscard]] -virtual_memory load_program(argument_parser& parser) +std::optional load_program(argument_parser& parser) { auto mode = load_normalised_mode::AUTO; if (parser.force_non_normalised()) { @@ -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); } } @@ -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 vmem) +{ + ui::terminal{parser, std::move(vmem)}.run(); +} + void run_program(virtual_memory vmem) { auto ctx = boost::asio::io_context{}; @@ -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 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)); } } } diff --git a/src/ui/terminal.cpp b/src/ui/terminal.cpp new file mode 100644 index 0000000..8f56ac9 --- /dev/null +++ b/src/ui/terminal.cpp @@ -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 +#include + +#include + +using namespace malbolge; + +struct ui::terminal::impl_t +{ + explicit impl_t(const argument_parser& arg_parser, + std::optional 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 vmem; +}; + +ui::terminal::terminal(const argument_parser& arg_parser, + std::optional vmem) : + impl_{std::make_shared(arg_parser, std::move(vmem))} +{} + +void ui::terminal::run() +{ + log::print(log::INFO, "Here"); + + initscr(); + printw("Hello world"); + refresh(); + endwin(); +} diff --git a/src/utility/argument_parser.cpp b/src/utility/argument_parser.cpp index 3ba8373..e308426 100644 --- a/src/utility/argument_parser.cpp +++ b/src/utility/argument_parser.cpp @@ -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(argc-1); @@ -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 @@ -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 @@ -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 @@ -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"; } diff --git a/src/virtual_cpu.cpp b/src/virtual_cpu.cpp index 5291302..0f25eb3 100644 --- a/src/virtual_cpu.cpp +++ b/src/virtual_cpu.cpp @@ -135,7 +135,7 @@ class virtual_cpu::impl_t : public std::enable_shared_from_this std::thread thread; virtual_memory vmem; - std::deque input_queue_; + std::deque input_queue; std::unordered_map bps; // vCPU Registers @@ -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(); @@ -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; } diff --git a/test/utility/argument_parser_test.cpp b/test/utility/argument_parser_test.cpp index 8e88401..3c9c9ea 100644 --- a/test/utility/argument_parser_test.cpp +++ b/test/utility/argument_parser_test.cpp @@ -42,6 +42,7 @@ BOOST_AUTO_TEST_CASE(no_args) BOOST_CHECK_EQUAL(ap.log_level(), log::ERROR); BOOST_CHECK(!ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); } BOOST_AUTO_TEST_CASE(unknown) @@ -67,6 +68,7 @@ BOOST_AUTO_TEST_CASE(help) BOOST_CHECK_EQUAL(ap.log_level(), log::ERROR); BOOST_CHECK(!ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); } } @@ -82,6 +84,7 @@ BOOST_AUTO_TEST_CASE(version) BOOST_CHECK_EQUAL(ap.log_level(), log::ERROR); BOOST_CHECK(!ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); } try { @@ -114,6 +117,7 @@ BOOST_AUTO_TEST_CASE(log_levels) BOOST_CHECK_EQUAL(ap.log_level(), static_cast(log::ERROR-l)); BOOST_CHECK(!ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); ++l; } @@ -153,6 +157,7 @@ BOOST_AUTO_TEST_CASE(force_nn) BOOST_CHECK_EQUAL(ap.log_level(), log::ERROR); BOOST_CHECK(ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); } BOOST_AUTO_TEST_CASE(file) @@ -167,6 +172,7 @@ BOOST_AUTO_TEST_CASE(file) BOOST_CHECK_EQUAL(ap.log_level(), log::ERROR); BOOST_CHECK(!ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); try { auto ap = arg_dispatcher({path, "-l"}); @@ -213,6 +219,7 @@ BOOST_AUTO_TEST_CASE(string) BOOST_CHECK_EQUAL(ap.log_level(), log::ERROR); BOOST_CHECK(!ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); try { auto ap = arg_dispatcher({"--string"}); @@ -235,6 +242,7 @@ BOOST_AUTO_TEST_CASE(logging_and_file) BOOST_CHECK_EQUAL(ap.log_level(), log::DEBUG); BOOST_CHECK(!ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); } BOOST_AUTO_TEST_CASE(logging_and_string) @@ -249,6 +257,7 @@ BOOST_AUTO_TEST_CASE(logging_and_string) BOOST_CHECK_EQUAL(ap.log_level(), log::DEBUG); BOOST_CHECK(!ap.force_non_normalised()); BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(!ap.interactive_mode()); } BOOST_AUTO_TEST_CASE(debugger_script) @@ -263,6 +272,7 @@ BOOST_AUTO_TEST_CASE(debugger_script) BOOST_CHECK(!ap.force_non_normalised()); BOOST_REQUIRE(ap.debugger_script()); BOOST_CHECK_EQUAL(*(ap.debugger_script()), script); + BOOST_CHECK(!ap.interactive_mode()); try { auto ap = arg_dispatcher({"--debugger-script"}); @@ -271,6 +281,22 @@ BOOST_AUTO_TEST_CASE(debugger_script) BOOST_CHECK_EQUAL(e.code().value(), static_cast(std::errc::invalid_argument)); } + + try { + auto ap = arg_dispatcher({"-i", "--debugger-script", script}); + BOOST_FAIL("Should have thrown"); + } catch (system_exception& e) { + BOOST_CHECK_EQUAL(e.code().value(), + static_cast(std::errc::invalid_argument)); + } + + try { + auto ap = arg_dispatcher({"--debugger-script", script, "-i"}); + BOOST_FAIL("Should have thrown"); + } catch (system_exception& e) { + BOOST_CHECK_EQUAL(e.code().value(), + static_cast(std::errc::invalid_argument)); + } } BOOST_AUTO_TEST_CASE(debugger_script_and_file) @@ -286,6 +312,7 @@ BOOST_AUTO_TEST_CASE(debugger_script_and_file) BOOST_CHECK(!ap.force_non_normalised()); BOOST_REQUIRE(ap.debugger_script()); BOOST_CHECK_EQUAL(*(ap.debugger_script()), script); + BOOST_CHECK(!ap.interactive_mode()); } BOOST_AUTO_TEST_CASE(program_source_streaming_operator) @@ -307,6 +334,19 @@ BOOST_AUTO_TEST_CASE(program_source_streaming_operator) ); } +BOOST_AUTO_TEST_CASE(interactive_mode) +{ + auto ap = arg_dispatcher({"-i"}); + BOOST_CHECK_EQUAL(ap.help(), false); + BOOST_CHECK_EQUAL(ap.version(), false); + BOOST_CHECK_EQUAL(ap.program().source, argument_parser::program_source::STDIN); + BOOST_CHECK_EQUAL(ap.program().data, ""s); + BOOST_CHECK_EQUAL(ap.log_level(), log::ERROR); + BOOST_CHECK(!ap.force_non_normalised()); + BOOST_CHECK(!ap.debugger_script()); + BOOST_CHECK(ap.interactive_mode()); +} + BOOST_AUTO_TEST_CASE(argument_parser_streaming_operator) { const auto expected = "Malbolge virtual machine v"s + project_version + @@ -319,7 +359,8 @@ BOOST_AUTO_TEST_CASE(argument_parser_streaming_operator) "\t-l\t\t\tLog level, repeat the l character for higher logging levels\n" "\t--string\t\tPass a string argument as the program to run\n" "\t--debugger-script\tRun the given debugger script on the program\n" - "\t--force-non-normalised\tOverride normalised program detection to force to non-normalised"; + "\t--force-non-normalised\tOverride normalised program detection to force to non-normalised\n" + "\t-i\t\t\tEnter an interactive terminal session"; auto ss = std::stringstream{}; ss << arg_dispatcher({}); diff --git a/test/utility/stream_helpers_test.cpp b/test/utility/stream_helpers_test.cpp new file mode 100644 index 0000000..a7e0621 --- /dev/null +++ b/test/utility/stream_helpers_test.cpp @@ -0,0 +1,38 @@ +/* Cam Mannett 2021 + * + * See LICENSE file + */ + +#include "malbolge/utility/stream_helpers.hpp" + +#include "test_helpers.hpp" + +using namespace malbolge; +using namespace std::string_literals; + +BOOST_AUTO_TEST_SUITE(stream_helpers_suite) + +BOOST_AUTO_TEST_CASE(data_available) +{ + auto ss = std::stringstream{}; + BOOST_CHECK(!utility::data_available(ss)); + + ss << "hello"; + BOOST_CHECK(utility::data_available(ss)); + + auto sink = ""s; + ss >> sink; + BOOST_CHECK(!utility::data_available(ss)); + BOOST_CHECK_EQUAL(sink, "hello"); + + ss.clear(); + ss << "goodbye"; + BOOST_CHECK(utility::data_available(ss)); + + sink = ""s; + ss >> sink; + BOOST_CHECK(!utility::data_available(ss)); + BOOST_CHECK_EQUAL(sink, "goodbye"); +} + +BOOST_AUTO_TEST_SUITE_END()