From deabd1865aca832d44d0ee8d07f403884b1cc1f5 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Mon, 30 Jan 2023 14:59:14 +0100 Subject: [PATCH 1/9] Introduced integrated documentation for builtin functions --- .../builtin_function_documentation.hh | 9 +++ musique/main.cc | 15 ++++ scripts/build.mk | 14 +++- scripts/document-builtin.py | 78 ++++++++++++++++--- 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 musique/interpreter/builtin_function_documentation.hh diff --git a/musique/interpreter/builtin_function_documentation.hh b/musique/interpreter/builtin_function_documentation.hh new file mode 100644 index 0000000..698c8ff --- /dev/null +++ b/musique/interpreter/builtin_function_documentation.hh @@ -0,0 +1,9 @@ +#ifndef MUSIQUE_BUILTIN_FUNCTION_DOCUMENTATION_HH +#define MUSIQUE_BUILTIN_FUNCTION_DOCUMENTATION_HH + +#include +#include + +std::optional find_documentation_for_builtin(std::string_view builtin_name); + +#endif diff --git a/musique/main.cc b/musique/main.cc index a62a4d6..6504986 100644 --- a/musique/main.cc +++ b/musique/main.cc @@ -17,6 +17,7 @@ #include #include #include +#include #ifdef _WIN32 extern "C" { @@ -418,6 +419,20 @@ static std::optional Main(std::span args) 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(); } diff --git a/scripts/build.mk b/scripts/build.mk index 0ef1b20..7601b09 100644 --- a/scripts/build.mk +++ b/scripts/build.mk @@ -1,4 +1,4 @@ -Release_Obj=$(addprefix bin/$(os)/,$(Obj)) +Release_Obj=$(addprefix bin/$(os)/,$(Obj)) bin/$(os)/builtin_function_documentation.o bin/$(os)/bestline.o: lib/bestline/bestline.c lib/bestline/bestline.h @echo "CC $@" @@ -12,7 +12,14 @@ bin/$(os)/$(Target): $(Release_Obj) bin/$(os)/main.o bin/$(os)/rtmidi.o $(Bestli @echo "CXX $@" @$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $(Release_Obj) bin/$(os)/rtmidi.o $(Bestline) $(LDFLAGS) $(LDLIBS) -Debug_Obj=$(addprefix bin/$(os)/debug/,$(Obj)) +bin/$(os)/builtin_function_documentation.o: bin/$(os)/builtin_function_documentation.cc + @echo "CXX $@" + @$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $< -c + +bin/$(os)/builtin_function_documentation.cc: musique/interpreter/builtin_functions.cc scripts/document-builtin.py + scripts/document-builtin.py -f cpp -o $@ musique/interpreter/builtin_functions.cc + +Debug_Obj=$(addprefix bin/$(os)/debug/,$(Obj)) bin/$(os)/debug/builtin_function_documentation.o bin/$(os)/debug/$(Target): $(Debug_Obj) bin/$(os)/debug/main.o bin/$(os)/rtmidi.o $(Bestline) @echo "CXX $@" @@ -22,3 +29,6 @@ bin/$(os)/debug/%.o: musique/%.cc @echo "CXX $@" @$(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) $(CPPFLAGS) -o $@ $< -c +bin/$(os)/debug/builtin_function_documentation.o: bin/$(os)/builtin_function_documentation.cc + @echo "CXX $@" + @$(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) $(CPPFLAGS) -o $@ $< -c diff --git a/scripts/document-builtin.py b/scripts/document-builtin.py index 4a77f46..c97e050 100755 --- a/scripts/document-builtin.py +++ b/scripts/document-builtin.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 import argparse import dataclasses -import string -import re import itertools -import typing +import json +import re +import string import subprocess +import typing MARKDOWN_CONVERTER = "lowdown -m 'shiftheadinglevelby=3'" CPP_FUNC_IDENT_ALLOWLIST = string.ascii_letters + string.digits + "_" @@ -60,6 +61,16 @@ HTML_SUFFIX = """ """ +FIND_DOCUMENTATION_FOR_BUILTIN = """ +std::optional find_documentation_for_builtin(std::string_view builtin_name) +{ + for (auto [name, doc] : names_to_documentation) { + if (builtin_name == name) + return doc; + } + return std::nullopt; +} +""" def warning(*args, prefix: str | None = None): if prefix is None: @@ -151,7 +162,9 @@ def builtins_from_file(source_path: str) -> typing.Generator[Builtin, None, None yield builtin -def filter_builtins(builtins: list[Builtin]) -> typing.Generator[Builtin, None, None]: +def filter_builtins( + builtins: typing.Iterable[Builtin], +) -> typing.Generator[Builtin, None, None]: for builtin in builtins: if not builtin.documentation: warning(f"builtin '{builtin.implementation}' doesn't have documentation") @@ -167,7 +180,7 @@ def filter_builtins(builtins: list[Builtin]) -> typing.Generator[Builtin, None, def each_musique_name_occurs_once( builtins: typing.Iterable[Builtin], ) -> typing.Generator[Builtin, None, None]: - names = {} + names: dict[str, str] = {} for builtin in builtins: for name in builtin.names: if name in names: @@ -178,7 +191,7 @@ def each_musique_name_occurs_once( yield builtin -def generate_html_document(builtins: list[Builtin], output_path: str): +def generate_html_document(builtins: typing.Iterable[Builtin], output_path: str): with open(output_path, "w") as out: out.write(HTML_PREFIX) @@ -218,7 +231,40 @@ def generate_html_document(builtins: list[Builtin], output_path: str): out.write(HTML_SUFFIX) -def main(source_path: str, output_path: str): +def generate_cpp_documentation(builtins: typing.Iterable[Builtin], output_path: str): + # TODO Support markdown rendering and colors using `pretty` sublibrary + + def documentation_str_var(builtin: Builtin): + return f"{builtin.implementation}_doc" + + with open(output_path, "w") as out: + print("#include ", file=out) + print("#include \n", file=out) + + # 1. Generate strings with documentation + for builtin in builtins: + # FIXME json.dumps will probably produce valid C++ strings for most cases, + # but can we ensure that will output valid strings for all cases? + print( + "static constexpr std::string_view %s = %s;" + % (documentation_str_var(builtin), json.dumps(builtin.documentation)), + file=out, + ) + print("", file=out) + + # 2. Generate array mapping from name to documentation variable + names_to_documentation = list(sorted((name, documentation_str_var(builtin)) for builtin in builtins for name in builtin.names)) + print("static constexpr std::array, %d> names_to_documentation = {" % (len(names_to_documentation), ), file=out) + for name, doc in names_to_documentation: + print(" std::pair { std::string_view(%s), %s }," % (json.dumps(name), doc), file=out) + print("};", file=out); + + # 3. Generate function that given builtin name results in documentation string + print(FIND_DOCUMENTATION_FOR_BUILTIN, file=out) + + + +def main(source_path: str, output_path: str, format: typing.Literal["html", "cpp"]): "Generates documentaiton from file source_path and saves in output_path" builtins = builtins_from_file(source_path) @@ -226,7 +272,10 @@ def main(source_path: str, output_path: str): builtins = each_musique_name_occurs_once(builtins) builtins = sorted(list(builtins), key=lambda builtin: builtin.names[0]) - generate_html_document(builtins, output_path) + if format == "md": + generate_html_document(builtins, output_path) + else: + generate_cpp_documentation(builtins, output_path) if __name__ == "__main__": @@ -247,10 +296,21 @@ if __name__ == "__main__": required=True, help="path for standalone HTML file containing generated documentation", ) + parser.add_argument( + "-f", + "--format", + type=str, + default="md", + help="output format. One of {html, cpp} are allowed, where HTML yields standalone docs, and C++ mode yields integrated docs", + ) PROGRAM_NAME = parser.prog args = parser.parse_args() assert len(args.source) == 1 assert len(args.output) == 1 + assert args.format in ( + "html", + "cpp", + ), "Only C++ and HTML output formats are supported" - main(source_path=args.source[0], output_path=args.output[0]) + main(source_path=args.source[0], output_path=args.output[0], format=args.format) From aaf6e6ec0c2dfaf19a41c10583814c225ae4228e Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Mon, 20 Feb 2023 17:37:30 +0100 Subject: [PATCH 2/9] 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) { From 83ede4ef813ba2069174a732d511a99cc64285f2 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Mon, 20 Feb 2023 18:16:20 +0100 Subject: [PATCH 3/9] Support complex syntax for command line arguments Support parameters prefixed by '-' or '--'. Support parameters in form 'key=value' --- config.mk | 2 +- musique/cmd.cc | 114 +++++++++++++++++++++++++++++++++--------------- musique/cmd.hh | 3 +- musique/main.cc | 26 +++-------- 4 files changed, 89 insertions(+), 56 deletions(-) diff --git a/config.mk b/config.mk index 2a6688d..52b23f3 100644 --- a/config.mk +++ b/config.mk @@ -13,7 +13,7 @@ VERSION := $(MAJOR).$(MINOR).$(PATCH)-dev+$(COMMIT) CXXFLAGS:=$(CXXFLAGS) -std=c++20 -Wall -Wextra -Werror=switch -Werror=return-type -Werror=unused-result CPPFLAGS:=$(CPPFLAGS) -DMusique_Version='"$(VERSION)"' \ - -Ilib/expected/ -I. -Ilib/bestline/ -Ilib/rtmidi/ -Ilib/link/include -Ilib/asio/include/ + -Ilib/expected/ -I. -Ilib/bestline/ -Ilib/rtmidi/ -Ilib/link/include -Ilib/asio/include/ -Ilib/edit_distance.cc/ LDFLAGS=-flto LDLIBS= -lpthread diff --git a/musique/cmd.cc b/musique/cmd.cc index 6778968..5c189d3 100644 --- a/musique/cmd.cc +++ b/musique/cmd.cc @@ -50,7 +50,6 @@ static constexpr std::array all_parameters = [] { std::cout << *maybe_docs << std::endl; return; } - std::cerr << "musique: error: cannot find documentation for given builtin" << std::endl; std::exit(1); }; @@ -99,42 +98,89 @@ static constexpr std::array all_parameters = [] { }; }(); -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; +// Supported types of argument input: +// With arity = 0 +// -i -j -k ≡ --i --j --k ≡ i j k +// With arity = 1 +// -i 1 -j 2 ≡ --i 1 --j 2 ≡ i 1 j 2 ≡ --i=1 --j=2 +// Arity ≥ 2 is not supported +std::optional cmd::accept_commandline_argument(std::vector &runnables, std::span &args) +{ + if (args.empty()) { + return std::nullopt; } - return false; + // This struct when function returns automatically ajust number of arguments used + struct Fix_Args_Array + { + std::string_view name() const { return m_name; } + + std::optional value() const + { + if (m_has_value) { m_success = m_value_consumed = true; return m_value; } + return std::nullopt; + } + + void mark_success() { m_success = true; } + + ~Fix_Args_Array() { args = args.subspan(int(m_success) + (!m_packed * int(m_value_consumed))); } + + std::span &args; + std::string_view m_name = {}; + std::string_view m_value = {}; + bool m_has_value = false; + bool m_packed = false; + mutable bool m_success = false; + mutable bool m_value_consumed = false; + } state { .args = args }; + + std::string_view s = args.front(); + if (s.starts_with("--")) s.remove_prefix(2); + if (s.starts_with('-')) s.remove_prefix(1); + + state.m_name = s; + + if (auto p = s.find('='); p != std::string_view::npos) { + state.m_name = s.substr(0, p); + state.m_value = s.substr(p+1); + state.m_has_value = true; + state.m_packed = true; + } else if (args.size() >= 2) { + state.m_has_value = true; + state.m_value = args[1]; + } + + for (auto const& [name, handler] : all_parameters) { + if (name != state.name()) { + continue; + } + std::visit(Overloaded { + [&state](Empty_Argument const& h) { + state.mark_success(); + h(); + }, + [&state, name=name](Requires_Argument const& h) { + auto arg = state.value(); + if (!arg) { + std::cerr << "musique: error: option " << std::quoted(name) << " requires an argument" << std::endl; + std::exit(1); + } + h(*arg); + }, + [&state, &runnables, name=name](Defines_Code const& h) { + auto arg = state.value(); + if (!arg) { + std::cerr << "musique: error: option " << std::quoted(name) << " requires an argument" << std::endl; + std::exit(1); + } + runnables.push_back(h(*arg)); + } + }, handler); + return std::nullopt; + } + + return state.name(); } diff --git a/musique/cmd.hh b/musique/cmd.hh index 8285be7..6f9af6f 100644 --- a/musique/cmd.hh +++ b/musique/cmd.hh @@ -1,6 +1,7 @@ #ifndef MUSIQUE_CMD_HH #define MUSIQUE_CMD_HH +#include #include #include #include @@ -22,7 +23,7 @@ namespace cmd }; /// Accept and execute next command line argument with its parameters if it has any - bool accept_commandline_argument(std::vector &runnables, std::span &args); + std::optional 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); diff --git a/musique/main.cc b/musique/main.cc index ccb1034..81b222a 100644 --- a/musique/main.cc +++ b/musique/main.cc @@ -321,35 +321,21 @@ static Result handle_repl_session_commands(std::string_view input, Runner /// Fancy main that supports Result forwarding on error (Try macro) static std::optional Main(std::span args) { + enable_repl = args.empty(); + if (cmd::is_tty() && getenv("NO_COLOR") == nullptr) { pretty::terminal_mode(); } - std::vector runnables; - 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; + while (args.size()) if (auto failed = cmd::accept_commandline_argument(runnables, args)) { + std::cerr << "musique: error: Failed to recognize parameter " << std::quoted(*failed) << std::endl; + cmd::print_close_matches(args.front()); + std::exit(1); } - /* - if (arg == "--ast") { - ast_only_mode = true; - continue; - } - - if (arg == "--version" || arg == "-v") { - std::cout << Musique_Version << std::endl; - return {}; - } - */ - Runner runner; for (auto const& [type, argument] : runnables) { From a7ed7a0d654d10d78eb1dd061421a8245c8bd298 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Mon, 20 Feb 2023 18:33:32 +0100 Subject: [PATCH 4/9] Proper usage message --- CHANGELOG.md | 2 +- musique/main.cc | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8743ac4..40aaeac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Builtin documentation for builtin functions display from repl and command line +- Builtin documentation for builtin functions display from repl and command line. - Suggestions which command line parameters user may wanted to use ### Changed diff --git a/musique/main.cc b/musique/main.cc index 81b222a..ad28015 100644 --- a/musique/main.cc +++ b/musique/main.cc @@ -39,23 +39,26 @@ static unsigned repl_line_number = 1; [[noreturn]] void usage() { std::cerr << - "usage: musique [filename]\n" - " where filename is path to file with Musique code that will be executed\n" - " where options are:\n" - " -c,--run CODE\n" - " executes given code\n" - " -I,--interactive,--repl\n" - " enables interactive mode even when another code was passed\n" + "usage: musique [subcommand]...\n" + " where available subcommands are:\n" + " run FILENAME\n" + " executes given file\n" "\n" - " -f,--as-function FILENAME\n" - " deffer file execution and turn it into a file\n" + " fun FILENAME\n" + " load file as function\n" "\n" - " --ast\n" - " prints ast for given code\n" + " repl\n" + " enter interactive enviroment\n" "\n" - " -v,--version\n" + " version\n" " prints Musique interpreter version\n" "\n" + " doc BUILTIN\n" + " print documentation for given builtin function\n" + "\n" + " help\n" + " prints this message\n" + "\n" "Thanks to:\n" " Sy Brand, https://sybrand.ink/, creator of tl::expected https://github.com/TartanLlama/expected\n" " Justine Tunney, https://justinetunney.com, creator of bestline readline library https://github.com/jart/bestline\n" From c0f021e57fc50cf8f0a56ae2cbf16fcd1742fdb6 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Sat, 4 Mar 2023 19:00:15 +0100 Subject: [PATCH 5/9] Introduce internal parameters; better condition for enabling repl --- musique/cmd.cc | 61 ++++++++++++++++++++++++++++++++++--------------- musique/main.cc | 9 +++----- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/musique/cmd.cc b/musique/cmd.cc index 5c189d3..eb702fa 100644 --- a/musique/cmd.cc +++ b/musique/cmd.cc @@ -10,6 +10,7 @@ #include #include #include +#include #ifdef _WIN32 extern "C" { @@ -60,7 +61,12 @@ static constexpr std::array all_parameters = [] { Empty_Argument print_version = [] { std::cout << Musique_Version << std::endl; }; Empty_Argument print_help = usage; - using Entry = std::pair; + struct Entry + { + std::string_view name; + Parameter handler; + bool internal = false; + }; // First entry for given action type should always be it's cannonical name return std::array { @@ -94,7 +100,11 @@ static constexpr std::array all_parameters = [] { Entry { "version", print_version }, Entry { "v", print_version }, - Entry { "ast", set_ast_only_mode }, + Entry { + .name = "ast", + .handler = set_ast_only_mode, + .internal = true, + }, }; }(); @@ -151,8 +161,8 @@ std::optional cmd::accept_commandline_argument(std::vector cmd::accept_commandline_argument(std::vector shown; + std::vector 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); + for (auto const& p : closest) { + if (std::find(shown.begin(), shown.end(), std::string(p.name)) == shown.end()) { + shown.push_back(std::string(p.name)); } } } + + if (shown.empty()) { + void *previous = nullptr; + std::cout << "Available subcommands are:\n"; + for (auto const& p : all_parameters) { + auto handler_p = std::visit([](auto const& h) { return reinterpret_cast(h); }, p.handler); + if (std::exchange(previous, handler_p) == handler_p || p.internal) { + continue; + } + std::cout << " " << p.name << '\n'; + } + + } else { + std::cout << "The most similar commands are:\n"; + for (auto const& name : shown) { + std::cout << " " << name << '\n'; + } + } } bool cmd::is_tty() diff --git a/musique/main.cc b/musique/main.cc index ad28015..fb2eaae 100644 --- a/musique/main.cc +++ b/musique/main.cc @@ -368,14 +368,11 @@ static std::optional Main(std::span args) } } - enable_repl = enable_repl || std::all_of(runnables.begin(), runnables.end(), - [](cmd::Run const& run) { - return run.type == cmd::Run::Deffered_File; - }); + enable_repl = enable_repl || (!runnables.empty() && std::all_of(runnables.begin(), runnables.end(), + [](cmd::Run const& run) { return run.type == cmd::Run::Deffered_File; })); - if (runnables.empty() || enable_repl) { + if (enable_repl) { repl_line_number = 1; - enable_repl = true; #ifndef _WIN32 bestlineSetCompletionCallback(completion); #else From 20a6779e2f916e1ed68532bdde775b5e15ec1812 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Sat, 4 Mar 2023 20:09:58 +0100 Subject: [PATCH 6/9] Usage generated from parameters short descriptions Additionally printing short descriptions with list of proposed commands --- musique/cmd.cc | 237 +++++++++++++++++++++++++++++++++------------- musique/cmd.hh | 3 + musique/main.cc | 33 ------- musique/pretty.cc | 20 ++-- musique/pretty.hh | 3 + 5 files changed, 192 insertions(+), 104 deletions(-) diff --git a/musique/cmd.cc b/musique/cmd.cc index eb702fa..7104551 100644 --- a/musique/cmd.cc +++ b/musique/cmd.cc @@ -6,11 +6,13 @@ #include #include #include +#include #include +#include #include #include -#include #include +#include #ifdef _WIN32 extern "C" { @@ -30,84 +32,132 @@ 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 }; - }; +static 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 }; - }; +static 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 }; - }; +static Defines_Code provide_file = [](std::string_view fname) -> cmd::Run { + return { .type = Run::File, .argument = fname }; +}; +static 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); +}; - 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); - }; +static Empty_Argument set_interactive_mode = [] { enable_repl = true; }; +static Empty_Argument set_ast_only_mode = [] { ast_only_mode = true; }; - Empty_Argument set_interactive_mode = [] { enable_repl = true; }; - Empty_Argument set_ast_only_mode = [] { ast_only_mode = true; }; +static Empty_Argument print_version = [] { std::cout << Musique_Version << std::endl; }; +static Empty_Argument print_help = usage; - Empty_Argument print_version = [] { std::cout << Musique_Version << std::endl; }; - Empty_Argument print_help = usage; +struct Entry +{ + std::string_view name; + Parameter handler; + bool internal = false; - struct Entry + void* handler_ptr() const { - std::string_view name; - Parameter handler; - bool internal = false; - }; + return std::visit([](auto p) { return reinterpret_cast(p); }, handler); + } - // 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 }, + size_t arguments() const + { + return std::visit([](R(*)(A...)) { return sizeof...(A); }, handler); + } +}; - Entry { "run", provide_file }, - Entry { "r", provide_file }, - Entry { "exec", provide_file }, +// First entry for given action type should always be it's cannonical name +static auto all_parameters = std::array { + Entry { "fun", provide_function }, + Entry { "def", provide_function }, + Entry { "f", provide_function }, + Entry { "func", provide_function }, + Entry { "function", provide_function }, - Entry { "repl", set_interactive_mode }, - Entry { "i", set_interactive_mode }, - Entry { "interactive", set_interactive_mode }, + Entry { "run", provide_file }, + Entry { "r", provide_file }, + Entry { "exec", provide_file }, + Entry { "load", provide_file }, - Entry { "doc", show_docs }, - Entry { "d", show_docs }, - Entry { "docs", show_docs }, + Entry { "repl", set_interactive_mode }, + Entry { "i", set_interactive_mode }, + Entry { "interactive", set_interactive_mode }, - Entry { "inline", provide_inline_code }, - Entry { "c", provide_inline_code }, - Entry { "code", provide_inline_code }, + Entry { "doc", show_docs }, + Entry { "d", show_docs }, + Entry { "docs", show_docs }, - Entry { "help", print_help }, - Entry { "?", print_help }, - Entry { "/?", print_help }, - Entry { "h", print_help }, + Entry { "inline", provide_inline_code }, + Entry { "c", provide_inline_code }, + Entry { "code", provide_inline_code }, - Entry { "version", print_version }, - Entry { "v", print_version }, + Entry { "help", print_help }, + Entry { "?", print_help }, + Entry { "/?", print_help }, + Entry { "h", print_help }, - Entry { - .name = "ast", - .handler = set_ast_only_mode, - .internal = true, - }, - }; -}(); + Entry { "version", print_version }, + Entry { "v", print_version }, + Entry { + .name = "ast", + .handler = set_ast_only_mode, + .internal = true, + }, +}; + +struct Documentation_For_Handler_Entry +{ + void *handler; + std::string_view short_documentation{}; + std::string_view long_documentation{}; +}; + +static auto documentation_for_handler = std::array { + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(provide_function), + .short_documentation = "load file as function", + }, + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(provide_file), + .short_documentation = "execute given file", + }, + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(set_interactive_mode), + .short_documentation = "enable interactive mode", + }, + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(print_help), + .short_documentation = "print help", + }, + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(print_version), + .short_documentation = "print version information", + }, + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(show_docs), + .short_documentation = "print documentation for given builtin", + }, + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(provide_inline_code), + .short_documentation = "run code from an argument", + }, + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(set_ast_only_mode), + .short_documentation = "don't run code, print AST of it", + }, +}; // Supported types of argument input: // With arity = 0 @@ -193,6 +243,23 @@ std::optional cmd::accept_commandline_argument(std::vectorhandler_ptr()); +} void cmd::print_close_matches(std::string_view arg) { @@ -224,19 +291,59 @@ void cmd::print_close_matches(std::string_view arg) void *previous = nullptr; std::cout << "Available subcommands are:\n"; for (auto const& p : all_parameters) { - auto handler_p = std::visit([](auto const& h) { return reinterpret_cast(h); }, p.handler); + auto handler_p = p.handler_ptr(); if (std::exchange(previous, handler_p) == handler_p || p.internal) { continue; } - std::cout << " " << p.name << '\n'; + std::cout << " " << p.name << " - " << find_documentation_for_handler(handler_p).short_documentation << '\n'; } - } else { std::cout << "The most similar commands are:\n"; for (auto const& name : shown) { - std::cout << " " << name << '\n'; + std::cout << " " << name << " - " << find_documentation_for_parameter(name).short_documentation; } } + + std::cout << "Invoke 'musique help' to read more about available commands\n"; +} + +void cmd::usage() +{ + std::cerr << "usage: " << pretty::begin_bold << "musique" << pretty::end << " [subcommand]...\n"; + std::cerr << " where available subcommands are:\n"; + + decltype(std::optional(all_parameters.begin())) previous = std::nullopt; + + for (auto it = all_parameters.begin();; ++it) { + if (it != all_parameters.end() && it->internal) + continue; + + if (it == all_parameters.end() || (previous && it->handler_ptr() != (*previous)->handler_ptr())) { + auto &e = **previous; + switch (e.arguments()) { + break; case 0: std::cerr << '\n'; + break; case 1: std::cerr << " ARG\n"; + break; default: unreachable(); + } + + std::cerr << " " + << find_documentation_for_handler(e.handler_ptr()).short_documentation + << "\n\n"; + } + + if (it == all_parameters.end()) { + break; + } + + if (previous && (**previous).handler_ptr() == it->handler_ptr()) { + std::cerr << ", " << it->name; + } else { + std::cerr << " " << pretty::begin_bold << it->name << pretty::end; + } + previous = it; + } + + std::exit(2); } bool cmd::is_tty() diff --git a/musique/cmd.hh b/musique/cmd.hh index 6f9af6f..a5c7041 100644 --- a/musique/cmd.hh +++ b/musique/cmd.hh @@ -30,6 +30,9 @@ namespace cmd /// Recognize if stdout is connected to terminal bool is_tty(); + + [[noreturn]] + void usage(); } #endif // MUSIQUE_CMD_HH diff --git a/musique/main.cc b/musique/main.cc index fb2eaae..455a69b 100644 --- a/musique/main.cc +++ b/musique/main.cc @@ -35,39 +35,6 @@ static unsigned repl_line_number = 1; #define Ignore(Call) do { auto const ignore_ ## __LINE__ = (Call); (void) ignore_ ## __LINE__; } while(0) -/// Print usage and exit -[[noreturn]] void usage() -{ - std::cerr << - "usage: musique [subcommand]...\n" - " where available subcommands are:\n" - " run FILENAME\n" - " executes given file\n" - "\n" - " fun FILENAME\n" - " load file as function\n" - "\n" - " repl\n" - " enter interactive enviroment\n" - "\n" - " version\n" - " prints Musique interpreter version\n" - "\n" - " doc BUILTIN\n" - " print documentation for given builtin function\n" - "\n" - " help\n" - " prints this message\n" - "\n" - "Thanks to:\n" - " Sy Brand, https://sybrand.ink/, creator of tl::expected https://github.com/TartanLlama/expected\n" - " Justine Tunney, https://justinetunney.com, creator of bestline readline library https://github.com/jart/bestline\n" - " Gary P. Scavone, http://www.music.mcgill.ca/~gary/, creator of rtmidi https://github.com/thestk/rtmidi\n" - " Creators of ableton/link, https://github.com/Ableton/link\n" - ; - std::exit(1); -} - void print_repl_help() { std::cout << diff --git a/musique/pretty.cc b/musique/pretty.cc index 55d5ee2..d4dde6c 100644 --- a/musique/pretty.cc +++ b/musique/pretty.cc @@ -8,10 +8,11 @@ extern "C" { namespace starters { - static std::string_view Error; - static std::string_view Path; + static std::string_view Bold; static std::string_view Comment; static std::string_view End; + static std::string_view Error; + static std::string_view Path; } std::ostream& pretty::begin_error(std::ostream& os) @@ -29,6 +30,11 @@ std::ostream& pretty::begin_comment(std::ostream& os) return os << starters::Comment; } +std::ostream& pretty::begin_bold(std::ostream& os) +{ + return os << starters::Bold; +} + std::ostream& pretty::end(std::ostream& os) { return os << starters::End; @@ -56,16 +62,18 @@ void pretty::terminal_mode() #endif - starters::Error = "\x1b[31;1m"; - starters::Path = "\x1b[34;1m"; + starters::Bold = "\x1b[1m"; starters::Comment = "\x1b[30;1m"; starters::End = "\x1b[0m"; + starters::Error = "\x1b[31;1m"; + starters::Path = "\x1b[34;1m"; } void pretty::no_color_mode() { - starters::Error = {}; - starters::Path = {}; + starters::Bold = {}; starters::Comment = {}; starters::End = {}; + starters::Error = {}; + starters::Path = {}; } diff --git a/musique/pretty.hh b/musique/pretty.hh index bda4be4..657746e 100644 --- a/musique/pretty.hh +++ b/musique/pretty.hh @@ -15,6 +15,9 @@ namespace pretty /// Mark start of printing a comment std::ostream& begin_comment(std::ostream&); + /// Mark start of printing with bold face + std::ostream& begin_bold(std::ostream&); + /// Mark end of any above std::ostream& end(std::ostream&); From a2d8f28cd14c9d18c43d3279e150bfcf970d81da Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Sat, 4 Mar 2023 20:15:58 +0100 Subject: [PATCH 7/9] More colorfull error messages --- musique/cmd.cc | 13 ++++++++----- musique/main.cc | 9 ++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/musique/cmd.cc b/musique/cmd.cc index 7104551..8be884c 100644 --- a/musique/cmd.cc +++ b/musique/cmd.cc @@ -50,7 +50,8 @@ static Requires_Argument show_docs = [](std::string_view builtin) { std::cout << *maybe_docs << std::endl; return; } - std::cerr << "musique: error: cannot find documentation for given builtin" << std::endl; + std::cerr << pretty::begin_error << "musique: error:" << pretty::end; + std::cerr << " cannot find documentation for given builtin" << std::endl; std::exit(1); }; @@ -223,7 +224,8 @@ std::optional cmd::accept_commandline_argument(std::vector cmd::accept_commandline_argument(std::vector Main(std::span args) while (args.size()) if (auto failed = cmd::accept_commandline_argument(runnables, args)) { - std::cerr << "musique: error: Failed to recognize parameter " << std::quoted(*failed) << std::endl; + std::cerr << pretty::begin_error << "musique: error:" << pretty::end; + std::cerr << " Failed to recognize parameter " << std::quoted(*failed) << std::endl; cmd::print_close_matches(args.front()); std::exit(1); } @@ -320,7 +321,8 @@ static std::optional Main(std::span args) eternal_sources.emplace_back(std::istreambuf_iterator(std::cin), std::istreambuf_iterator()); } else { if (not fs::exists(path)) { - std::cerr << "musique: error: couldn't open file: " << path << std::endl; + std::cerr << pretty::begin_error << "musique: error:" << pretty::end; + std::cerr << " couldn't open file: " << path << std::endl; std::exit(1); } std::ifstream source_file{fs::path(path)}; @@ -381,7 +383,8 @@ static std::optional Main(std::span args) if (command.starts_with(':')) { command.remove_prefix(1); if (!Try(handle_repl_session_commands(command, runner))) { - std::cerr << "musique: error: unrecognized REPL command '" << command << '\'' << std::endl; + std::cerr << pretty::begin_error << "musique: error:" << pretty::end; + std::cerr << " unrecognized REPL command '" << command << '\'' << std::endl; } continue; } From 92e5c3169cc8570a37c1ce1815c49da1c13a1710 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Sat, 4 Mar 2023 21:23:13 +0100 Subject: [PATCH 8/9] Started `musique man` implementation: structure of a page implemented --- musique/cmd.cc | 90 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/musique/cmd.cc b/musique/cmd.cc index 8be884c..8dd9888 100644 --- a/musique/cmd.cc +++ b/musique/cmd.cc @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -61,6 +62,9 @@ static Empty_Argument set_ast_only_mode = [] { ast_only_mode = true; }; static Empty_Argument print_version = [] { std::cout << Musique_Version << std::endl; }; static Empty_Argument print_help = usage; +[[noreturn]] +static void print_manpage(); + struct Entry { std::string_view name; @@ -80,17 +84,17 @@ struct Entry // First entry for given action type should always be it's cannonical name static auto all_parameters = std::array { + Entry { "run", provide_file }, + Entry { "r", provide_file }, + Entry { "exec", provide_file }, + Entry { "load", provide_file }, + 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 { "load", provide_file }, - Entry { "repl", set_interactive_mode }, Entry { "i", set_interactive_mode }, Entry { "interactive", set_interactive_mode }, @@ -99,6 +103,8 @@ static auto all_parameters = std::array { Entry { "d", show_docs }, Entry { "docs", show_docs }, + Entry { "man", print_manpage }, + Entry { "inline", provide_inline_code }, Entry { "c", provide_inline_code }, Entry { "code", provide_inline_code }, @@ -129,34 +135,65 @@ static auto documentation_for_handler = std::array { Documentation_For_Handler_Entry { .handler = reinterpret_cast(provide_function), .short_documentation = "load file as function", + .long_documentation = + "Loads given file, placing it inside a function. The main use for this mechanism is\n" + "to delay execution of a file e.g. to play it using synchronization infrastructure.\n" + "Name of function is derived from file name, replacing special characters with underscores.\n" + "New name is reported when entering interactive mode." }, Documentation_For_Handler_Entry { .handler = reinterpret_cast(provide_file), .short_documentation = "execute given file", + .long_documentation = + "Run provided Musique source file." }, Documentation_For_Handler_Entry { .handler = reinterpret_cast(set_interactive_mode), .short_documentation = "enable interactive mode", + .long_documentation = + "Enables interactive mode. It's enabled by default when provided without arguments or\n" + "when all arguments are files loaded as functions." }, Documentation_For_Handler_Entry { .handler = reinterpret_cast(print_help), .short_documentation = "print help", + .long_documentation = + "Prints short version of help, to provide version easy for quick lookup by the user." }, Documentation_For_Handler_Entry { .handler = reinterpret_cast(print_version), .short_documentation = "print version information", + .long_documentation = + "Prints version of Musique, following Semantic Versioning.\n" + "It's either '..' for official releases or\n" + "'..-dev+gc' for self-build releases." }, Documentation_For_Handler_Entry { .handler = reinterpret_cast(show_docs), .short_documentation = "print documentation for given builtin", + .long_documentation = + "Prints documentation for given builtin function (function predefined by language).\n" + "Documentation is in Markdown format and can be passed to render." }, Documentation_For_Handler_Entry { .handler = reinterpret_cast(provide_inline_code), .short_documentation = "run code from an argument", + .long_documentation = + "Runs code passed as next argument. Same rules apply as for code inside a file." }, Documentation_For_Handler_Entry { .handler = reinterpret_cast(set_ast_only_mode), .short_documentation = "don't run code, print AST of it", + .long_documentation = + "Parameter made for internal usage. Instead of executing provided code,\n" + "prints program syntax tree." + }, + Documentation_For_Handler_Entry { + .handler = reinterpret_cast(print_manpage), + .short_documentation = "print man page source code to standard output", + .long_documentation = + "Prints Man page document to standard output of Musique full command line interface.\n" + "One can view it with 'musique man > /tmp/musique.1; man /tmp/musique.1'" }, }; @@ -349,6 +386,49 @@ void cmd::usage() std::exit(2); } +void print_manpage() +{ + auto const ymd = std::chrono::year_month_day( + std::chrono::floor( + std::chrono::system_clock::now() + ) + ); + + std::cout << ".TH MUSIQUE 1 " + << int(ymd.year()) << '-' + << std::setfill('0') << std::setw(2) << unsigned(ymd.month()) << '-' + << std::setfill('0') << std::setw(2) << unsigned(ymd.day()) + << " Linux Linux\n"; + + std::cout << R"troff(.SH NAME +musique \- interactive, musical programming language +.SH SYNOPSIS +.B musique +[ +SUBCOMMANDS +] +.SH DESCRIPTION +Musique is an interpreted, interactive, musical domain specific programming language +that allows for algorythmic music composition, live-coding and orchestra performing. +.SH SUBCOMMANDS +TODO +.SH ENVIROMENT +TODO: NO_COLORS env +.SH FILES +TODO: History file +.SH EXAMPLES +.TP +musique \-c "play (c5 + up 12)" +Plays all semitones in 5th octave +.TP +musique run examples/ode-to-joy.mq +Play Ode to Joy written as Musique source code in examples/ode-to-joy.mq +)troff"; + + + std::exit(0); +} + bool cmd::is_tty() { #ifdef _WIN32 From 719e8d4f26f0393f0d242ad8260db32fb3629bc6 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Sun, 5 Mar 2023 01:10:43 +0100 Subject: [PATCH 9/9] Report similar names for builtins that user is searching for --- CHANGELOG.md | 3 +- musique/cmd.cc | 66 ++++++++++++++----- .../builtin_function_documentation.hh | 5 ++ scripts/document-builtin.py | 33 +++++++++- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40aaeac..12da1a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Builtin documentation for builtin functions display from repl and command line. +- Builtin documentation for builtin functions display from repl and command line (`musique doc `) +- Man documentation for commandline interface builtin (`musique man`) - Suggestions which command line parameters user may wanted to use ### Changed diff --git a/musique/cmd.cc b/musique/cmd.cc index 8dd9888..c1482cc 100644 --- a/musique/cmd.cc +++ b/musique/cmd.cc @@ -15,6 +15,8 @@ #include #include +// TODO: Command line parameters full documentation in other then man pages format. Maybe HTML generation? + #ifdef _WIN32 extern "C" { #include @@ -53,6 +55,11 @@ static Requires_Argument show_docs = [](std::string_view builtin) { } std::cerr << pretty::begin_error << "musique: error:" << pretty::end; std::cerr << " cannot find documentation for given builtin" << std::endl; + + std::cerr << "Similar ones are:" << std::endl; + for (auto similar : similar_names_to_builtin(builtin)) { + std::cerr << " " << similar << '\n'; + } std::exit(1); }; @@ -347,11 +354,12 @@ void cmd::print_close_matches(std::string_view arg) std::cout << "\nInvoke 'musique help' to read more about available commands\n"; } -void cmd::usage() +static inline void iterate_over_documentation( + std::ostream& out, + std::string_view Documentation_For_Handler_Entry::* handler, + std::string_view prefix, + std::ostream&(*first)(std::ostream&, std::string_view name)) { - std::cerr << "usage: " << pretty::begin_bold << "musique" << pretty::end << " [subcommand]...\n"; - std::cerr << " where available subcommands are:\n"; - decltype(std::optional(all_parameters.begin())) previous = std::nullopt; for (auto it = all_parameters.begin();; ++it) { @@ -361,14 +369,12 @@ void cmd::usage() if (it == all_parameters.end() || (previous && it->handler_ptr() != (*previous)->handler_ptr())) { auto &e = **previous; switch (e.arguments()) { - break; case 0: std::cerr << '\n'; - break; case 1: std::cerr << " ARG\n"; + break; case 0: out << '\n'; + break; case 1: out << " ARG\n"; break; default: unreachable(); } - std::cerr << " " - << find_documentation_for_handler(e.handler_ptr()).short_documentation - << "\n\n"; + out << prefix << find_documentation_for_handler(e.handler_ptr()).*handler << "\n\n"; } if (it == all_parameters.end()) { @@ -376,12 +382,24 @@ void cmd::usage() } if (previous && (**previous).handler_ptr() == it->handler_ptr()) { - std::cerr << ", " << it->name; + out << ", " << it->name; } else { - std::cerr << " " << pretty::begin_bold << it->name << pretty::end; + first(out, it->name); } previous = it; } +} + +void cmd::usage() +{ + std::cerr << "usage: " << pretty::begin_bold << "musique" << pretty::end << " [subcommand]...\n"; + std::cerr << " where available subcommands are:\n"; + + iterate_over_documentation(std::cerr, &Documentation_For_Handler_Entry::short_documentation, " ", + [](std::ostream& out, std::string_view name) -> std::ostream& + { + return out << " " << pretty::begin_bold << name << pretty::end; + }); std::exit(2); } @@ -411,11 +429,28 @@ SUBCOMMANDS Musique is an interpreted, interactive, musical domain specific programming language that allows for algorythmic music composition, live-coding and orchestra performing. .SH SUBCOMMANDS -TODO -.SH ENVIROMENT -TODO: NO_COLORS env +All subcommands can be expressed in three styles: -i arg -j -k +.I or +--i=arg --j --k +.I or +i arg j k +)troff"; + + iterate_over_documentation(std::cout, &Documentation_For_Handler_Entry::long_documentation, {}, + [](std::ostream& out, std::string_view name) -> std::ostream& + { + return out << ".TP\n" << name; + }); + + std::cout << R"troff(.SH ENVIROMENT +.TP +NO_COLOR +This enviroment variable overrides standard Musique color behaviour. +When it's defined, it disables colors and ensures they are not enabled. .SH FILES -TODO: History file +.TP +History file +History file for interactive mode is kept in XDG_DATA_HOME (or similar on other operating systems). .SH EXAMPLES .TP musique \-c "play (c5 + up 12)" @@ -425,7 +460,6 @@ musique run examples/ode-to-joy.mq Play Ode to Joy written as Musique source code in examples/ode-to-joy.mq )troff"; - std::exit(0); } diff --git a/musique/interpreter/builtin_function_documentation.hh b/musique/interpreter/builtin_function_documentation.hh index 698c8ff..42071f1 100644 --- a/musique/interpreter/builtin_function_documentation.hh +++ b/musique/interpreter/builtin_function_documentation.hh @@ -3,7 +3,12 @@ #include #include +#include std::optional find_documentation_for_builtin(std::string_view builtin_name); + +/// Returns top 4 similar names to required +std::vector similar_names_to_builtin(std::string_view builtin_name); + #endif diff --git a/scripts/document-builtin.py b/scripts/document-builtin.py index c97e050..4ed4314 100755 --- a/scripts/document-builtin.py +++ b/scripts/document-builtin.py @@ -70,6 +70,28 @@ std::optional find_documentation_for_builtin(std::string_view } return std::nullopt; } + +std::vector similar_names_to_builtin(std::string_view builtin_name) +{ + auto minimum_distance = std::numeric_limits::max(); + + std::array, 4> closest; + std::partial_sort_copy( + names_to_documentation.begin(), names_to_documentation.end(), + closest.begin(), closest.end(), + [&minimum_distance, builtin_name](auto const& lhs, auto const& rhs) { + auto const lhs_score = edit_distance(builtin_name, lhs.first); + auto const rhs_score = edit_distance(builtin_name, rhs.first); + minimum_distance = std::min({ minimum_distance, lhs_score, rhs_score }); + return lhs_score < rhs_score; + } + ); + + std::vector result; + result.resize(4); + std::transform(closest.begin(), closest.end(), result.begin(), [](auto const& p) { return p.first; }); + return result; +} """ def warning(*args, prefix: str | None = None): @@ -238,8 +260,15 @@ def generate_cpp_documentation(builtins: typing.Iterable[Builtin], output_path: return f"{builtin.implementation}_doc" with open(output_path, "w") as out: - print("#include ", file=out) - print("#include \n", file=out) + includes = [ + "algorithm", + "array", + "edit_distance.hh", + "musique/interpreter/builtin_function_documentation.hh", + "vector", + ] + for include in includes: + print(f"#include <{include}>", file=out) # 1. Generate strings with documentation for builtin in builtins: