Merge branch 'integrated-documentation' into staged-0.5

This commit is contained in:
Robert Bendun 2023-03-05 01:13:15 +01:00
commit 943b065626
13 changed files with 794 additions and 167 deletions

View File

@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Builtin documentation for builtin functions display from repl and command line (`musique doc <builtin>`)
- Man documentation for commandline interface builtin (`musique man`)
- 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] ## [0.4.0]
### Added ### Added

View File

@ -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 CXXFLAGS:=$(CXXFLAGS) -std=c++20 -Wall -Wextra -Werror=switch -Werror=return-type -Werror=unused-result
CPPFLAGS:=$(CPPFLAGS) -DMusique_Version='"$(VERSION)"' \ 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 LDFLAGS=-flto
LDLIBS= -lpthread LDLIBS= -lpthread

5
lib/edit_distance.cc/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
test
test.cc
Makefile
.cache
compile_commands.json

View File

@ -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.

View File

@ -0,0 +1,76 @@
// Copyright 2023 Robert Bendun <robert@bendun.cc>
//
// 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 <algorithm>
#include <array>
#include <concepts>
#include <iterator>
#include <numeric>
#include <ranges>
#include <type_traits>
#include <vector>
template<std::random_access_iterator S, std::random_access_iterator T>
requires std::equality_comparable_with<
std::iter_value_t<S>,
std::iter_value_t<T>
>
constexpr int edit_distance(S s, unsigned m, T t, unsigned n)
{
std::array<std::vector<int>, 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<Range1>,
std::ranges::range_value_t<Range2>
>
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)
);
}

474
musique/cmd.cc Normal file
View File

@ -0,0 +1,474 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <edit_distance.hh>
#include <iomanip>
#include <iostream>
#include <limits>
#include <musique/cmd.hh>
#include <musique/common.hh>
#include <musique/errors.hh>
#include <musique/interpreter/builtin_function_documentation.hh>
#include <musique/pretty.hh>
#include <set>
#include <unordered_set>
#include <utility>
#include <variant>
// TODO: Command line parameters full documentation in other then man pages format. Maybe HTML generation?
#ifdef _WIN32
extern "C" {
#include <io.h>
}
#else
#include <unistd.h>
#endif
using Empty_Argument = void(*)();
using Requires_Argument = void(*)(std::string_view);
using Defines_Code = cmd::Run(*)(std::string_view);
using Parameter = std::variant<Empty_Argument, Requires_Argument, Defines_Code>;
using namespace cmd;
// from musique/main.cc:
extern bool enable_repl;
extern bool ast_only_mode;
static Defines_Code provide_function = [](std::string_view fname) -> cmd::Run {
return { .type = Run::Deffered_File, .argument = fname };
};
static Defines_Code provide_inline_code = [](std::string_view code) -> cmd::Run {
return { .type = Run::Argument, .argument = code };
};
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 << 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);
};
static Empty_Argument set_interactive_mode = [] { enable_repl = true; };
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;
Parameter handler;
bool internal = false;
void* handler_ptr() const
{
return std::visit([](auto p) { return reinterpret_cast<void*>(p); }, handler);
}
size_t arguments() const
{
return std::visit([]<typename R, typename ...A>(R(*)(A...)) { return sizeof...(A); }, handler);
}
};
// 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 { "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 { "man", print_manpage },
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 {
.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<void*>(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<void*>(provide_file),
.short_documentation = "execute given file",
.long_documentation =
"Run provided Musique source file."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(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<void*>(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<void*>(print_version),
.short_documentation = "print version information",
.long_documentation =
"Prints version of Musique, following Semantic Versioning.\n"
"It's either '<major>.<minor>.<patch>' for official releases or\n"
"'<major>.<minor>.<patch>-dev+gc<commit hash>' for self-build releases."
},
Documentation_For_Handler_Entry {
.handler = reinterpret_cast<void*>(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<void*>(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<void*>(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<void*>(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'"
},
};
// 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<std::string_view> cmd::accept_commandline_argument(std::vector<cmd::Run> &runnables, std::span<char const*> &args)
{
if (args.empty()) {
return std::nullopt;
}
// 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<std::string_view> 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<char const*> &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& p : all_parameters) {
if (p.name != state.name()) {
continue;
}
std::visit(Overloaded {
[&state](Empty_Argument const& h) {
state.mark_success();
h();
},
[&state, p](Requires_Argument const& h) {
auto arg = state.value();
if (!arg) {
std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " option " << std::quoted(p.name) << " requires an argument" << std::endl;
std::exit(1);
}
h(*arg);
},
[&state, &runnables, p](Defines_Code const& h) {
auto arg = state.value();
if (!arg) {
std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " option " << std::quoted(p.name) << " requires an argument" << std::endl;
std::exit(1);
}
runnables.push_back(h(*arg));
}
}, p.handler);
return std::nullopt;
}
return state.name();
}
Documentation_For_Handler_Entry find_documentation_for_handler(void *handler)
{
auto it = std::find_if(documentation_for_handler.begin(), documentation_for_handler.end(),
[=](Documentation_For_Handler_Entry const& e) { return e.handler == handler; });
ensure(it != documentation_for_handler.end(), "Parameter handler doesn't have matching documentation");
return *it;
}
Documentation_For_Handler_Entry find_documentation_for_parameter(std::string_view param)
{
auto entry = std::find_if(all_parameters.begin(), all_parameters.end(),
[=](auto const& e) { return e.name == param; });
ensure(entry != all_parameters.end(), "Cannot find parameter that maches given name");
return find_documentation_for_handler(entry->handler_ptr());
}
void cmd::print_close_matches(std::string_view arg)
{
auto minimum_distance = std::numeric_limits<int>::max();
std::array<typename decltype(all_parameters)::value_type, 3> 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.name);
auto const rhs_score = edit_distance(arg, rhs.name);
minimum_distance = std::min({ minimum_distance, lhs_score, rhs_score });
return lhs_score < rhs_score;
}
);
std::vector<std::string> shown;
if (minimum_distance <= 3) {
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 = p.handler_ptr();
if (std::exchange(previous, handler_p) == handler_p || p.internal) {
continue;
}
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 << " - " << find_documentation_for_parameter(name).short_documentation << '\n';
}
}
std::cout << "\nInvoke 'musique help' to read more about available commands\n";
}
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))
{
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: out << '\n';
break; case 1: out << " ARG\n";
break; default: unreachable();
}
out << prefix << find_documentation_for_handler(e.handler_ptr()).*handler << "\n\n";
}
if (it == all_parameters.end()) {
break;
}
if (previous && (**previous).handler_ptr() == it->handler_ptr()) {
out << ", " << it->name;
} else {
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);
}
void print_manpage()
{
auto const ymd = std::chrono::year_month_day(
std::chrono::floor<std::chrono::days>(
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
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
.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)"
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
return _isatty(STDOUT_FILENO);
#else
return isatty(fileno(stdout));
#endif
}

39
musique/cmd.hh Normal file
View File

@ -0,0 +1,39 @@
#ifndef MUSIQUE_CMD_HH
#define MUSIQUE_CMD_HH
#include <optional>
#include <span>
#include <string_view>
#include <variant>
#include <vector>
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
std::optional<std::string_view> accept_commandline_argument(std::vector<cmd::Run> &runnables, std::span<char const*> &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();
[[noreturn]]
void usage();
}
#endif // MUSIQUE_CMD_HH

View File

@ -0,0 +1,14 @@
#ifndef MUSIQUE_BUILTIN_FUNCTION_DOCUMENTATION_HH
#define MUSIQUE_BUILTIN_FUNCTION_DOCUMENTATION_HH
#include <optional>
#include <string_view>
#include <vector>
std::optional<std::string_view> find_documentation_for_builtin(std::string_view builtin_name);
/// Returns top 4 similar names to required
std::vector<std::string_view> similar_names_to_builtin(std::string_view builtin_name);
#endif

View File

@ -1,13 +1,13 @@
#include <charconv> #include <charconv>
#include <cstdio>
#include <cstring>
#include <edit_distance.hh>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <span> #include <musique/cmd.hh>
#include <thread>
#include <cstring>
#include <cstdio>
#include <musique/format.hh> #include <musique/format.hh>
#include <musique/interpreter/builtin_function_documentation.hh>
#include <musique/interpreter/env.hh> #include <musique/interpreter/env.hh>
#include <musique/interpreter/interpreter.hh> #include <musique/interpreter/interpreter.hh>
#include <musique/lexer/lines.hh> #include <musique/lexer/lines.hh>
@ -17,13 +17,11 @@
#include <musique/try.hh> #include <musique/try.hh>
#include <musique/unicode.hh> #include <musique/unicode.hh>
#include <musique/value/block.hh> #include <musique/value/block.hh>
#include <span>
#include <thread>
#include <unordered_set>
#ifdef _WIN32 #ifndef _WIN32
extern "C" {
#include <io.h>
}
#else
#include <unistd.h>
extern "C" { extern "C" {
#include <bestline.h> #include <bestline.h>
} }
@ -31,66 +29,12 @@ extern "C" {
namespace fs = std::filesystem; namespace fs = std::filesystem;
static bool quiet_mode = false; bool ast_only_mode = false;
static bool ast_only_mode = false; bool enable_repl = false;
static bool enable_repl = false;
static unsigned repl_line_number = 1; static unsigned repl_line_number = 1;
#define Ignore(Call) do { auto const ignore_ ## __LINE__ = (Call); (void) ignore_ ## __LINE__; } while(0) #define Ignore(Call) do { auto const ignore_ ## __LINE__ = (Call); (void) ignore_ ## __LINE__; } while(0)
/// Pop string from front of an array
template<typename T = std::string_view>
static T pop(std::span<char const*> &span)
{
auto element = span.front();
span = span.subspan(1);
if constexpr (std::is_same_v<T, std::string_view>) {
return element;
} else if constexpr (std::is_arithmetic_v<T>) {
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<T>, "Unsupported type for pop operation");
}
}
/// Print usage and exit
[[noreturn]] void usage()
{
std::cerr <<
"usage: musique <options> [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"
"\n"
" -f,--as-function FILENAME\n"
" deffer file execution and turn it into a file\n"
"\n"
" --ast\n"
" prints ast for given code\n"
"\n"
" -v,--version\n"
" prints Musique interpreter version\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() void print_repl_help()
{ {
std::cout << std::cout <<
@ -245,15 +189,6 @@ void completion(char const* buf, bestlineCompletions *lc)
} }
#endif #endif
bool is_tty()
{
#ifdef _WIN32
return _isatty(STDOUT_FILENO);
#else
return isatty(fileno(stdout));
#endif
}
/// Handles commands inside REPL session (those starting with ':') /// Handles commands inside REPL session (those starting with ':')
/// ///
/// Returns if one of command matched /// Returns if one of command matched
@ -352,84 +287,30 @@ static Result<bool> handle_repl_session_commands(std::string_view input, Runner
return false; return false;
} }
/// Fancy main that supports Result forwarding on error (Try macro) /// Fancy main that supports Result forwarding on error (Try macro)
static std::optional<Error> Main(std::span<char const*> args) static std::optional<Error> Main(std::span<char const*> args)
{ {
if (is_tty() && getenv("NO_COLOR") == nullptr) { enable_repl = args.empty();
if (cmd::is_tty() && getenv("NO_COLOR") == nullptr) {
pretty::terminal_mode(); pretty::terminal_mode();
} }
/// Describes all arguments that will be run std::vector<cmd::Run> runnables;
struct Run
{
enum Type
{
File,
Argument,
Deffered_File
} type;
std::string_view argument;
};
std::vector<Run> runnables;
while (not args.empty()) { while (args.size()) if (auto failed = cmd::accept_commandline_argument(runnables, args)) {
std::string_view arg = pop(args); std::cerr << pretty::begin_error << "musique: error:" << pretty::end;
std::cerr << " Failed to recognize parameter " << std::quoted(*failed) << std::endl;
if (arg == "-" || !arg.starts_with('-')) { cmd::print_close_matches(args.front());
runnables.push_back({ .type = Run::File, .argument = std::move(arg) });
continue;
}
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 == "-h" || arg == "--help") {
usage();
}
std::cerr << "musique: error: unrecognized command line option: " << arg << std::endl;
std::exit(1); std::exit(1);
} }
Runner runner; Runner runner;
for (auto const& [type, argument] : runnables) { for (auto const& [type, argument] : runnables) {
if (type == Run::Argument) { if (type == cmd::Run::Argument) {
Lines::the.add_line("<arguments>", argument, repl_line_number); Lines::the.add_line("<arguments>", argument, repl_line_number);
Try(runner.run(argument, "<arguments>")); Try(runner.run(argument, "<arguments>"));
repl_line_number++; repl_line_number++;
@ -440,7 +321,8 @@ static std::optional<Error> Main(std::span<char const*> args)
eternal_sources.emplace_back(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>()); eternal_sources.emplace_back(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
} else { } else {
if (not fs::exists(path)) { 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::exit(1);
} }
std::ifstream source_file{fs::path(path)}; std::ifstream source_file{fs::path(path)};
@ -448,21 +330,18 @@ static std::optional<Error> Main(std::span<char const*> args)
} }
Lines::the.add_file(std::string(path), eternal_sources.back()); 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)); Try(runner.run(eternal_sources.back(), path));
} else { } else {
Try(runner.deffered_file(eternal_sources.back(), argument)); Try(runner.deffered_file(eternal_sources.back(), argument));
} }
} }
enable_repl = enable_repl || std::all_of(runnables.begin(), runnables.end(), enable_repl = enable_repl || (!runnables.empty() && std::all_of(runnables.begin(), runnables.end(),
[](Run const& run) { [](cmd::Run const& run) { return run.type == cmd::Run::Deffered_File; }));
return run.type == Run::Deffered_File;
});
if (runnables.empty() || enable_repl) { if (enable_repl) {
repl_line_number = 1; repl_line_number = 1;
enable_repl = true;
#ifndef _WIN32 #ifndef _WIN32
bestlineSetCompletionCallback(completion); bestlineSetCompletionCallback(completion);
#else #else
@ -504,7 +383,8 @@ static std::optional<Error> Main(std::span<char const*> args)
if (command.starts_with(':')) { if (command.starts_with(':')) {
command.remove_prefix(1); command.remove_prefix(1);
if (!Try(handle_repl_session_commands(command, runner))) { 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; continue;
} }

View File

@ -8,10 +8,11 @@ extern "C" {
namespace starters namespace starters
{ {
static std::string_view Error; static std::string_view Bold;
static std::string_view Path;
static std::string_view Comment; static std::string_view Comment;
static std::string_view End; static std::string_view End;
static std::string_view Error;
static std::string_view Path;
} }
std::ostream& pretty::begin_error(std::ostream& os) std::ostream& pretty::begin_error(std::ostream& os)
@ -29,6 +30,11 @@ std::ostream& pretty::begin_comment(std::ostream& os)
return os << starters::Comment; return os << starters::Comment;
} }
std::ostream& pretty::begin_bold(std::ostream& os)
{
return os << starters::Bold;
}
std::ostream& pretty::end(std::ostream& os) std::ostream& pretty::end(std::ostream& os)
{ {
return os << starters::End; return os << starters::End;
@ -56,16 +62,18 @@ void pretty::terminal_mode()
#endif #endif
starters::Error = "\x1b[31;1m"; starters::Bold = "\x1b[1m";
starters::Path = "\x1b[34;1m";
starters::Comment = "\x1b[30;1m"; starters::Comment = "\x1b[30;1m";
starters::End = "\x1b[0m"; starters::End = "\x1b[0m";
starters::Error = "\x1b[31;1m";
starters::Path = "\x1b[34;1m";
} }
void pretty::no_color_mode() void pretty::no_color_mode()
{ {
starters::Error = {}; starters::Bold = {};
starters::Path = {};
starters::Comment = {}; starters::Comment = {};
starters::End = {}; starters::End = {};
starters::Error = {};
starters::Path = {};
} }

View File

@ -15,6 +15,9 @@ namespace pretty
/// Mark start of printing a comment /// Mark start of printing a comment
std::ostream& begin_comment(std::ostream&); std::ostream& begin_comment(std::ostream&);
/// Mark start of printing with bold face
std::ostream& begin_bold(std::ostream&);
/// Mark end of any above /// Mark end of any above
std::ostream& end(std::ostream&); std::ostream& end(std::ostream&);

View File

@ -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 bin/$(os)/bestline.o: lib/bestline/bestline.c lib/bestline/bestline.h
@echo "CC $@" @echo "CC $@"
@ -12,7 +12,14 @@ bin/$(os)/$(Target): $(Release_Obj) bin/$(os)/main.o bin/$(os)/rtmidi.o $(Bestli
@echo "CXX $@" @echo "CXX $@"
@$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $(Release_Obj) bin/$(os)/rtmidi.o $(Bestline) $(LDFLAGS) $(LDLIBS) @$(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) bin/$(os)/debug/$(Target): $(Debug_Obj) bin/$(os)/debug/main.o bin/$(os)/rtmidi.o $(Bestline)
@echo "CXX $@" @echo "CXX $@"
@ -22,3 +29,6 @@ bin/$(os)/debug/%.o: musique/%.cc
@echo "CXX $@" @echo "CXX $@"
@$(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) $(CPPFLAGS) -o $@ $< -c @$(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

View File

@ -1,11 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import dataclasses import dataclasses
import string
import re
import itertools import itertools
import typing import json
import re
import string
import subprocess import subprocess
import typing
MARKDOWN_CONVERTER = "lowdown -m 'shiftheadinglevelby=3'" MARKDOWN_CONVERTER = "lowdown -m 'shiftheadinglevelby=3'"
CPP_FUNC_IDENT_ALLOWLIST = string.ascii_letters + string.digits + "_" CPP_FUNC_IDENT_ALLOWLIST = string.ascii_letters + string.digits + "_"
@ -60,6 +61,38 @@ HTML_SUFFIX = """
</html> </html>
""" """
FIND_DOCUMENTATION_FOR_BUILTIN = """
std::optional<std::string_view> 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;
}
std::vector<std::string_view> similar_names_to_builtin(std::string_view builtin_name)
{
auto minimum_distance = std::numeric_limits<int>::max();
std::array<std::pair<std::string_view, std::string_view>, 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<std::string_view> 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): def warning(*args, prefix: str | None = None):
if prefix is None: if prefix is None:
@ -151,7 +184,9 @@ def builtins_from_file(source_path: str) -> typing.Generator[Builtin, None, None
yield builtin 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: for builtin in builtins:
if not builtin.documentation: if not builtin.documentation:
warning(f"builtin '{builtin.implementation}' doesn't have documentation") warning(f"builtin '{builtin.implementation}' doesn't have documentation")
@ -167,7 +202,7 @@ def filter_builtins(builtins: list[Builtin]) -> typing.Generator[Builtin, None,
def each_musique_name_occurs_once( def each_musique_name_occurs_once(
builtins: typing.Iterable[Builtin], builtins: typing.Iterable[Builtin],
) -> typing.Generator[Builtin, None, None]: ) -> typing.Generator[Builtin, None, None]:
names = {} names: dict[str, str] = {}
for builtin in builtins: for builtin in builtins:
for name in builtin.names: for name in builtin.names:
if name in names: if name in names:
@ -178,7 +213,7 @@ def each_musique_name_occurs_once(
yield builtin 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: with open(output_path, "w") as out:
out.write(HTML_PREFIX) out.write(HTML_PREFIX)
@ -218,7 +253,47 @@ def generate_html_document(builtins: list[Builtin], output_path: str):
out.write(HTML_SUFFIX) 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:
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:
# 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<std::pair<std::string_view, std::string_view>, %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" "Generates documentaiton from file source_path and saves in output_path"
builtins = builtins_from_file(source_path) builtins = builtins_from_file(source_path)
@ -226,7 +301,10 @@ def main(source_path: str, output_path: str):
builtins = each_musique_name_occurs_once(builtins) builtins = each_musique_name_occurs_once(builtins)
builtins = sorted(list(builtins), key=lambda builtin: builtin.names[0]) builtins = sorted(list(builtins), key=lambda builtin: builtin.names[0])
if format == "md":
generate_html_document(builtins, output_path) generate_html_document(builtins, output_path)
else:
generate_cpp_documentation(builtins, output_path)
if __name__ == "__main__": if __name__ == "__main__":
@ -247,10 +325,21 @@ if __name__ == "__main__":
required=True, required=True,
help="path for standalone HTML file containing generated documentation", 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 PROGRAM_NAME = parser.prog
args = parser.parse_args() args = parser.parse_args()
assert len(args.source) == 1 assert len(args.source) == 1
assert len(args.output) == 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)