From aaf6e6ec0c2dfaf19a41c10583814c225ae4228e Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Mon, 20 Feb 2023 17:37:30 +0100 Subject: [PATCH] New parameter passing convention With suggestions on wrong parameter names --- CHANGELOG.md | 9 ++ lib/edit_distance.cc/.gitignore | 5 + lib/edit_distance.cc/LICENSE | 19 +++ lib/edit_distance.cc/edit_distance.hh | 76 +++++++++++ musique/cmd.cc | 179 ++++++++++++++++++++++++++ musique/cmd.hh | 35 +++++ musique/main.cc | 145 ++++----------------- 7 files changed, 350 insertions(+), 118 deletions(-) create mode 100644 lib/edit_distance.cc/.gitignore create mode 100644 lib/edit_distance.cc/LICENSE create mode 100644 lib/edit_distance.cc/edit_distance.hh create mode 100644 musique/cmd.cc create mode 100644 musique/cmd.hh diff --git a/CHANGELOG.md b/CHANGELOG.md index 313139f..8743ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Builtin documentation for builtin functions display from repl and command line +- Suggestions which command line parameters user may wanted to use + +### Changed + +- New parameter passing convention for command line invocation. `musique help` to learn how it changed + ## [0.4.0] ### Added diff --git a/lib/edit_distance.cc/.gitignore b/lib/edit_distance.cc/.gitignore new file mode 100644 index 0000000..95d97eb --- /dev/null +++ b/lib/edit_distance.cc/.gitignore @@ -0,0 +1,5 @@ +test +test.cc +Makefile +.cache +compile_commands.json diff --git a/lib/edit_distance.cc/LICENSE b/lib/edit_distance.cc/LICENSE new file mode 100644 index 0000000..ba5ebc5 --- /dev/null +++ b/lib/edit_distance.cc/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Robert Bendun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/edit_distance.cc/edit_distance.hh b/lib/edit_distance.cc/edit_distance.hh new file mode 100644 index 0000000..83ae045 --- /dev/null +++ b/lib/edit_distance.cc/edit_distance.hh @@ -0,0 +1,76 @@ +// Copyright 2023 Robert Bendun +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: + +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include +#include +#include +#include +#include +#include +#include +#include + +template +requires std::equality_comparable_with< + std::iter_value_t, + std::iter_value_t +> +constexpr int edit_distance(S s, unsigned m, T t, unsigned n) +{ + std::array, 2> memo; + auto *v0 = &memo[0]; + auto *v1 = &memo[1]; + + for (auto& v : memo) { + v.resize(n+1); + } + std::iota(v0->begin(), v0->end(), 0); + + for (auto i = 0u; i < m; ++i) { + (*v1)[0] = i+1; + for (auto j = 0u; j < n; ++j) { + auto const deletion_cost = (*v0)[j+1] + 1; + auto const insertion_cost = (*v1)[j] + 1; + auto const substitution_cost = (*v0)[j] + (s[i] != t[j]); + + (*v1)[j+1] = std::min({ deletion_cost, insertion_cost, substitution_cost }); + } + std::swap(v0, v1); + } + + return (*v0)[n]; +} + +template< + std::ranges::random_access_range Range1, + std::ranges::random_access_range Range2 +> +requires std::equality_comparable_with< + std::ranges::range_value_t, + std::ranges::range_value_t +> +constexpr int edit_distance(Range1 const& range1, Range2 const& range2) +{ + return edit_distance( + std::begin(range1), std::ranges::size(range1), + std::begin(range2), std::ranges::size(range2) + ); +} diff --git a/musique/cmd.cc b/musique/cmd.cc new file mode 100644 index 0000000..6778968 --- /dev/null +++ b/musique/cmd.cc @@ -0,0 +1,179 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +extern "C" { +#include +} +#else +#include +#endif + +using Empty_Argument = void(*)(); +using Requires_Argument = void(*)(std::string_view); +using Defines_Code = cmd::Run(*)(std::string_view); +using Parameter = std::variant; + +using namespace cmd; + +// from musique/main.cc: +extern bool enable_repl; +extern bool ast_only_mode; +extern void usage(); + +static constexpr std::array all_parameters = [] { + Defines_Code provide_function = [](std::string_view fname) -> cmd::Run { + return { .type = Run::Deffered_File, .argument = fname }; + }; + + Defines_Code provide_inline_code = [](std::string_view code) -> cmd::Run { + return { .type = Run::Argument, .argument = code }; + }; + + Defines_Code provide_file = [](std::string_view fname) -> cmd::Run { + return { .type = Run::File, .argument = fname }; + }; + + + Requires_Argument show_docs = [](std::string_view builtin) { + if (auto maybe_docs = find_documentation_for_builtin(builtin); maybe_docs) { + std::cout << *maybe_docs << std::endl; + return; + } + + std::cerr << "musique: error: cannot find documentation for given builtin" << std::endl; + std::exit(1); + }; + + Empty_Argument set_interactive_mode = [] { enable_repl = true; }; + Empty_Argument set_ast_only_mode = [] { ast_only_mode = true; }; + + Empty_Argument print_version = [] { std::cout << Musique_Version << std::endl; }; + Empty_Argument print_help = usage; + + using Entry = std::pair; + + // First entry for given action type should always be it's cannonical name + return std::array { + Entry { "fun", provide_function }, + Entry { "def", provide_function }, + Entry { "f", provide_function }, + Entry { "func", provide_function }, + Entry { "function", provide_function }, + + Entry { "run", provide_file }, + Entry { "r", provide_file }, + Entry { "exec", provide_file }, + + Entry { "repl", set_interactive_mode }, + Entry { "i", set_interactive_mode }, + Entry { "interactive", set_interactive_mode }, + + Entry { "doc", show_docs }, + Entry { "d", show_docs }, + Entry { "docs", show_docs }, + + Entry { "inline", provide_inline_code }, + Entry { "c", provide_inline_code }, + Entry { "code", provide_inline_code }, + + Entry { "help", print_help }, + Entry { "?", print_help }, + Entry { "/?", print_help }, + Entry { "h", print_help }, + + Entry { "version", print_version }, + Entry { "v", print_version }, + + Entry { "ast", set_ast_only_mode }, + }; +}(); + +bool cmd::accept_commandline_argument(std::vector &runnables, std::span &args) +{ + if (args.empty()) + return false; + + for (auto const& [name, handler] : all_parameters) { + // TODO Parameters starting with - or -- should be considered equal + if (name != args.front()) { + continue; + } + args = args.subspan(1); + std::visit(Overloaded { + [](Empty_Argument const& h) { + h(); + }, + [&args, name=name](Requires_Argument const& h) { + if (args.empty()) { + std::cerr << "musique: error: option " << std::quoted(name) << " requires an argument" << std::endl; + std::exit(1); + } + h(args.front()); + args = args.subspan(1); + }, + [&, name=name](Defines_Code const& h) { + if (args.empty()) { + std::cerr << "musique: error: option " << std::quoted(name) << " requires an argument" << std::endl; + std::exit(1); + } + runnables.push_back(h(args.front())); + args = args.subspan(1); + } + }, handler); + return true; + } + + return false; +} + + +void cmd::print_close_matches(std::string_view arg) +{ + auto minimum_distance = std::numeric_limits::max(); + + std::array closest; + + std::partial_sort_copy( + all_parameters.begin(), all_parameters.end(), + closest.begin(), closest.end(), + [&minimum_distance, arg](auto const& lhs, auto const& rhs) { + auto const lhs_score = edit_distance(arg, lhs.first); + auto const rhs_score = edit_distance(arg, rhs.first); + minimum_distance = std::min({ minimum_distance, lhs_score, rhs_score }); + return lhs_score < rhs_score; + } + ); + + std::cout << "The most similar commands are:\n"; + std::unordered_set shown; + if (minimum_distance <= 3) { + for (auto const& [ name, handler ] : closest) { + auto const handler_p = std::visit([](auto *v) { return reinterpret_cast(v); }, handler); + if (!shown.contains(handler_p)) { + std::cout << " " << name << std::endl; + shown.insert(handler_p); + } + } + } +} + +bool cmd::is_tty() +{ +#ifdef _WIN32 + return _isatty(STDOUT_FILENO); +#else + return isatty(fileno(stdout)); +#endif +} + diff --git a/musique/cmd.hh b/musique/cmd.hh new file mode 100644 index 0000000..8285be7 --- /dev/null +++ b/musique/cmd.hh @@ -0,0 +1,35 @@ +#ifndef MUSIQUE_CMD_HH +#define MUSIQUE_CMD_HH + +#include +#include +#include +#include + +namespace cmd +{ + /// Describes all arguments that will be run + struct Run + { + enum Type + { + File, + Argument, + Deffered_File + } type; + + std::string_view argument; + }; + + /// Accept and execute next command line argument with its parameters if it has any + bool accept_commandline_argument(std::vector &runnables, std::span &args); + + /// Print all arguments that are similar to one provided + void print_close_matches(std::string_view arg); + + /// Recognize if stdout is connected to terminal + bool is_tty(); +} + +#endif // MUSIQUE_CMD_HH + diff --git a/musique/main.cc b/musique/main.cc index 6504986..ccb1034 100644 --- a/musique/main.cc +++ b/musique/main.cc @@ -1,13 +1,13 @@ #include +#include +#include +#include #include #include #include -#include -#include -#include -#include - +#include #include +#include #include #include #include @@ -17,14 +17,11 @@ #include #include #include -#include +#include +#include +#include -#ifdef _WIN32 -extern "C" { -#include -} -#else -#include +#ifndef _WIN32 extern "C" { #include } @@ -32,36 +29,12 @@ extern "C" { namespace fs = std::filesystem; -static bool quiet_mode = false; -static bool ast_only_mode = false; -static bool enable_repl = false; +bool ast_only_mode = false; +bool enable_repl = false; static unsigned repl_line_number = 1; #define Ignore(Call) do { auto const ignore_ ## __LINE__ = (Call); (void) ignore_ ## __LINE__; } while(0) -/// Pop string from front of an array -template -static T pop(std::span &span) -{ - auto element = span.front(); - span = span.subspan(1); - - if constexpr (std::is_same_v) { - return element; - } else if constexpr (std::is_arithmetic_v) { - T result; - auto end = element + std::strlen(element); - auto [ptr, ec] = std::from_chars(element, end, result); - if (ec != decltype(ec){}) { - std::cout << "Expected natural number as argument" << std::endl; - std::exit(1); - } - return result; - } else { - static_assert(always_false, "Unsupported type for pop operation"); - } -} - /// Print usage and exit [[noreturn]] void usage() { @@ -246,15 +219,6 @@ void completion(char const* buf, bestlineCompletions *lc) } #endif -bool is_tty() -{ -#ifdef _WIN32 - return _isatty(STDOUT_FILENO); -#else - return isatty(fileno(stdout)); -#endif -} - /// Handles commands inside REPL session (those starting with ':') /// /// Returns if one of command matched @@ -353,98 +317,43 @@ static Result handle_repl_session_commands(std::string_view input, Runner return false; } + /// Fancy main that supports Result forwarding on error (Try macro) static std::optional Main(std::span args) { - if (is_tty() && getenv("NO_COLOR") == nullptr) { + if (cmd::is_tty() && getenv("NO_COLOR") == nullptr) { pretty::terminal_mode(); } - /// Describes all arguments that will be run - struct Run - { - enum Type - { - File, - Argument, - Deffered_File - } type; - std::string_view argument; - }; - std::vector runnables; + std::vector runnables; - while (not args.empty()) { - std::string_view arg = pop(args); - if (arg == "-" || !arg.starts_with('-')) { - runnables.push_back({ .type = Run::File, .argument = std::move(arg) }); - continue; + for (;;) if (!cmd::accept_commandline_argument(runnables, args)) { + if (args.size()) { + std::cerr << "musique: error: Failed to recognize parameter " << std::quoted(args.front()) << std::endl; + cmd::print_close_matches(args.front()); + std::exit(1); } + break; + } - if (arg == "-c" || arg == "--run") { - if (args.empty()) { - std::cerr << "musique: error: option " << arg << " requires an argument" << std::endl; - std::exit(1); - } - runnables.push_back({ .type = Run::Argument, .argument = pop(args) }); - continue; - } - - if (arg == "-f" || arg == "--as-function") { - if (args.empty()) { - std::cerr << "musique: error: option " << arg << " requires an argument" << std::endl; - std::exit(1); - } - runnables.push_back({ .type = Run::Deffered_File, .argument = pop(args) }); - continue; - } - - if (arg == "--quiet" || arg == "-q") { - quiet_mode = true; - continue; - } + /* if (arg == "--ast") { ast_only_mode = true; continue; } - if (arg == "--repl" || arg == "-I" || arg == "--interactive") { - enable_repl = true; - continue; - } - if (arg == "--version" || arg == "-v") { std::cout << Musique_Version << std::endl; return {}; } - - if (arg == "--doc" || arg == "-d") { - if (args.empty()) { - std::cerr << "musique: error: option " << arg << " requires an argument" << std::endl; - std::exit(1); - } - if (auto maybe_docs = find_documentation_for_builtin(pop(args)); maybe_docs) { - std::cout << *maybe_docs << std::endl; - return {}; - } else { - std::cerr << "musique: error: cannot find documentation for given builtin" << std::endl; - std::exit(1); - } - } - - if (arg == "-h" || arg == "--help") { - usage(); - } - - std::cerr << "musique: error: unrecognized command line option: " << arg << std::endl; - std::exit(1); - } + */ Runner runner; for (auto const& [type, argument] : runnables) { - if (type == Run::Argument) { + if (type == cmd::Run::Argument) { Lines::the.add_line("", argument, repl_line_number); Try(runner.run(argument, "")); repl_line_number++; @@ -463,7 +372,7 @@ static std::optional Main(std::span args) } Lines::the.add_file(std::string(path), eternal_sources.back()); - if (type == Run::File) { + if (type == cmd::Run::File) { Try(runner.run(eternal_sources.back(), path)); } else { Try(runner.deffered_file(eternal_sources.back(), argument)); @@ -471,8 +380,8 @@ static std::optional Main(std::span args) } enable_repl = enable_repl || std::all_of(runnables.begin(), runnables.end(), - [](Run const& run) { - return run.type == Run::Deffered_File; + [](cmd::Run const& run) { + return run.type == cmd::Run::Deffered_File; }); if (runnables.empty() || enable_repl) {