diff --git a/Makefile b/Makefile index 646a4ff..bd92fe7 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ Obj= \ builtin_functions.o \ builtin_operators.o \ context.o \ - environment.o \ + env.o \ errors.o \ format.o \ interpreter.o \ @@ -29,7 +29,7 @@ bin/bestline.o: lib/bestline/bestline.c lib/bestline/bestline.h @echo "CC $@" @$(CC) $< -c -O3 -o $@ -doc: Doxyfile src/*.cc include/*.hh +doc: Doxyfile musique/*.cc include/*.hh doxygen doc-open: doc @@ -44,7 +44,6 @@ release: bin/musique install: bin/musique scripts/install - .PHONY: clean doc doc-open all test unit-tests release install $(shell mkdir -p bin/debug/tests) diff --git a/config.mk b/config.mk index 4f82686..18a346b 100644 --- a/config.mk +++ b/config.mk @@ -1,7 +1,7 @@ MAKEFLAGS="-j $(grep -c ^processor /proc/cpuinfo)" CXXFLAGS:=$(CXXFLAGS) -std=c++20 -Wall -Wextra -Werror=switch -Werror=return-type -Werror=unused-result -Wno-maybe-uninitialized -CPPFLAGS:=$(CPPFLAGS) -Ilib/expected/ -Ilib/ut/ -Ilib/midi/include -Iinclude/ -Ilib/bestline/ +CPPFLAGS:=$(CPPFLAGS) -Ilib/expected/ -Ilib/ut/ -Ilib/midi/include -I. -Ilib/bestline/ RELEASE_FLAGS=-O3 DEBUG_FLAGS=-O0 -ggdb -fsanitize=undefined -DDebug diff --git a/include/musique.hh b/include/musique.hh deleted file mode 100644 index 032caa7..0000000 --- a/include/musique.hh +++ /dev/null @@ -1,1225 +0,0 @@ -#ifndef Musique_Header_HH -#define Musique_Header_HH - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#if defined(__cpp_lib_source_location) -#include -#endif - -// To make sure, that we don't collide with macro -#ifdef assert -#undef assert -#endif - -using namespace std::string_literals; -using namespace std::string_view_literals; - -using u8 = std::uint8_t; -using u16 = std::uint16_t; -using u32 = std::uint32_t; -using u64 = std::uint64_t; - -using i8 = std::int8_t; -using i16 = std::int16_t; -using i32 = std::int32_t; -using i64 = std::int64_t; - -using usize = std::size_t; -using isize = std::ptrdiff_t; - -/// \brief Location describes code position in `file line column` format. -/// It's used both to represent position in source files provided -// to interpreter and internal interpreter usage. -struct Location -{ - std::string_view filename = ""; ///< File that location is pointing to - usize line = 1; ///< Line number (1 based) that location is pointing to - usize column = 1; ///< Column number (1 based) that location is pointing to - - /// Advances line and column numbers based on provided rune - /// - /// If rune is newline, then column is reset to 1, and line number is incremented. - /// Otherwise column number is incremented. - /// - /// @param rune Rune from which column and line numbers advancements are made. - Location& advance(u32 rune); - - bool operator==(Location const& rhs) const = default; - - //! Creates location at default filename with specified line and column number - static Location at(usize line, usize column); - - // Used to describe location of function call in interpreter (internal use only) -#if defined(__cpp_lib_source_location) - static Location caller(std::source_location loc = std::source_location::current()); -#elif (__has_builtin(__builtin_FILE) and __has_builtin(__builtin_LINE)) - static Location caller(char const* file = __builtin_FILE(), usize line = __builtin_LINE()); -#else -#error Cannot implement Location::caller function - /// Returns location of call in interpreter source code. - /// - /// Example of reporting where `foo()` was beeing called: - /// @code - /// void foo(Location loc = Location::caller()) { std::cout << loc << '\n'; } - /// @endcode - static Location caller(); -#endif -}; - -std::ostream& operator<<(std::ostream& os, Location const& location); - -/// Error handling related functions and definitions -namespace errors -{ - /// When user puts emoji in the source code - struct Unrecognized_Character - { - u32 invalid_character; - }; - - /// When parser was expecting code but encountered end of file - struct Unexpected_Empty_Source - { - enum { Block_Without_Closing_Bracket } reason; - std::optional start; - }; - - /// When user passed numeric literal too big for numeric type - struct Failed_Numeric_Parsing - { - std::errc reason; - }; - - /// When user forgot semicolon or brackets - struct Expected_Expression_Separator_Before - { - std::string_view what; - }; - - /// When some keywords are not allowed in given context - struct Unexpected_Keyword - { - std::string_view keyword; - }; - - /// When user tried to use operator that was not defined - struct Undefined_Operator - { - std::string_view op; - }; - - /// When user tries to use operator with wrong arity of arguments - struct Wrong_Arity_Of - { - /// Type of operation - enum Type { Operator, Function } type; - - /// Name of operation - std::string_view name; - - /// Arity that was expected by given operation - size_t expected_arity; - - /// Arit that user provided - size_t actual_arity; - }; - - /// When user tried to call something that can't be called - struct Not_Callable - { - std::string_view type; - }; - - /// When user provides literal where identifier should be - struct Literal_As_Identifier - { - std::string_view type_name; - std::string_view source; - std::string_view context; - }; - - /// When user provides wrong type for given operation - struct Unsupported_Types_For - { - /// Type of operation - enum Type { Operator, Function } type; - - /// Name of operation - std::string_view name; - - /// Possible ways to use it correctly - std::vector possibilities; - }; - - /// When user tries to use variable that has not been defined yet. - struct Missing_Variable - { - /// Name of variable - std::string name; - }; - - /// When user tries to invoke some MIDI action but haven't established MIDI connection - struct Operation_Requires_Midi_Connection - { - /// If its input or output connection missing - bool is_input; - - /// Name of the operation that was beeing invoked - std::string name; - }; - - /// When user tries to get element from collection with index higher then collection size - struct Out_Of_Range - { - /// Index that was required by the user - size_t required_index; - - /// Size of accessed collection - size_t size; - }; - - struct Closing_Token_Without_Opening - { - enum { - Block = ']', - Paren = ')' - } type; - }; - - struct Arithmetic - { - enum Type - { - Division_By_Zero, - Fractional_Modulo, - Unable_To_Calculate_Modular_Multiplicative_Inverse - } type; - }; - - /// Collection of messages that are considered internal and should not be printed to the end user. - namespace internal - { - /// When encountered token that was supposed to be matched in higher branch of the parser - struct Unexpected_Token - { - /// Type of the token - std::string_view type; - - /// Source of the token - std::string_view source; - - /// Where this token was encountered that was unexpected? - std::string_view when; - }; - } - - /// All possible error types - using Details = std::variant< - Arithmetic, - Closing_Token_Without_Opening, - Expected_Expression_Separator_Before, - Failed_Numeric_Parsing, - Literal_As_Identifier, - Missing_Variable, - Not_Callable, - Operation_Requires_Midi_Connection, - Out_Of_Range, - Undefined_Operator, - Unexpected_Empty_Source, - Unexpected_Keyword, - Unrecognized_Character, - Unsupported_Types_For, - Wrong_Arity_Of, - internal::Unexpected_Token - >; -} - -/// All code related to pretty printing. Default mode is no_color -namespace pretty -{ - /// Mark start of printing an error - std::ostream& begin_error(std::ostream&); - - /// Mark start of printing a path - std::ostream& begin_path(std::ostream&); - - /// Mark start of printing a comment - std::ostream& begin_comment(std::ostream&); - - /// Mark end of any above - std::ostream& end(std::ostream&); - - /// Switch to colorful output via ANSI escape sequences - void terminal_mode(); - - /// Switch to colorless output (default one) - void no_color_mode(); -} - -/// Combine several lambdas into one for visiting std::variant -template -struct Overloaded : Lambdas... { using Lambdas::operator()...; }; - -/// Guards that program exits if condition does not hold -void assert(bool condition, std::string message, Location loc = Location::caller()); - -/// Marks part of code that was not implemented yet -[[noreturn]] void unimplemented(std::string_view message = {}, Location loc = Location::caller()); - -/// Marks location that should not be reached -[[noreturn]] void unreachable(Location loc = Location::caller()); - -/// Represents all recoverable error messages that interpreter can produce -struct Error -{ - /// Specific message details - errors::Details details; - - /// Location that coused all this trouble - std::optional location = std::nullopt; - - /// Return self with new location - Error with(Location) &&; -}; - -/// Error pretty printing -std::ostream& operator<<(std::ostream& os, Error const& err); - -/// Returns if provided thingy is a given template -template typename Template, typename> -struct is_template : std::false_type {}; - -template typename Template, typename ...T> -struct is_template> : std::true_type {}; - -/// Returns if provided thingy is a given template -template typename Template, typename T> -constexpr auto is_template_v = is_template::value; - -/// Holds either T or Error -template -struct [[nodiscard("This value may contain critical error, so it should NOT be ignored")]] Result : tl::expected -{ - using Storage = tl::expected; - - constexpr Result() = default; - - template requires (not std::is_void_v) && std::is_constructible_v - constexpr Result(Args&& ...args) - : Storage( T{ std::forward(args)... } ) - { - } - - template requires std::is_constructible_v - constexpr Result(Arg &&arg) - : Storage(std::forward(arg)) - { - } - - inline Result(Error error) - : Storage(tl::unexpected(std::move(error))) - { - } - - template - requires requires (Arg a) { - { Error { .details = std::move(a) } }; - } - inline Result(Arg a) - : Storage(tl::unexpected(Error { .details = std::move(a) })) - { - } - - // Internal function used for definition of Try macro - inline auto value() && - { - if constexpr (not std::is_void_v) { - // NOTE This line in ideal world should be `return Storage::value()` - // but C++ does not infer that this is rvalue context. - // `std::add_rvalue_reference_t::value()` - // also does not work, so this is probably the best way to express this: - return std::move(*static_cast(this)).value(); - } - } - - /// Fill error location if it's empty and we have an error - inline Result with_location(Location location) && - { - if (!Storage::has_value()) { - if (auto& target = Storage::error().location; !target || target == Location{}) { - target = location; - } - } - return *this; - } - - inline tl::expected to_expected() && - { - return *static_cast(this); - } - - template - requires is_template_v> - auto and_then(Map &&map) && - { - return std::move(*static_cast(this)).and_then( - [map = std::forward(map)](T &&value) { - return std::move(map)(std::move(value)).to_expected(); - }); - } - - using Storage::and_then; - - operator std::optional() && - { - return Storage::has_value() ? std::nullopt : std::optional(Storage::error()); - } -}; - -/// Abstraction over any value that are either value or error -/// -/// Inspired by P2561R0 -template -struct Try_Traits -{ - template - static constexpr bool is_ok(T const& v) { return Try_Traits::is_ok(v); } - - template - static constexpr auto yield_value(T&& v) { return Try_Traits::yield_value(std::forward(v)); } - - template - static constexpr auto yield_error(T&& v) { return Try_Traits::yield_error(std::forward(v)); } -}; - -template<> -struct Try_Traits> -{ - using Value_Type = std::nullopt_t; - using Error_Type = Error; - - static constexpr bool is_ok(std::optional const& o) - { - return not o.has_value(); - } - - static std::nullopt_t yield_value(std::optional&& err) - { - assert(not err.has_value(), "Trying to yield value from optional that contains error"); - return std::nullopt; - } - - static Error yield_error(std::optional&& err) - { - assert(err.has_value(), "Trying to yield value from optional that NOT constains error"); - return std::move(*err); - } -}; - -template -struct Try_Traits> -{ - using Value_Type = T; - using Error_Type = Error; - - static constexpr bool is_ok(Result const& o) - { - return o.has_value(); - } - - static auto yield_value(Result val) - { - assert(val.has_value(), "Trying to yield value from expected that contains error"); - if constexpr (std::is_void_v) { - } else { - return std::move(*val); - } - } - - static Error yield_error(Result&& val) - { - assert(not val.has_value(), "Trying to yield error from expected with value"); - return std::move(val.error()); - } -}; - -/// Shorthand for forwarding error values with Result type family. -/// -/// This implementation requires C++ language extension: statement expressions -/// It's supported by GCC and Clang, other compilers i don't know. -/// Inspired by SerenityOS TRY macro -#define Try(Value) \ - ({ \ - auto try_value = (Value); \ - using Trait [[maybe_unused]] = Try_Traits>; \ - if (not Trait::is_ok(try_value)) [[unlikely]] \ - return Trait::yield_error(std::move(try_value)); \ - Trait::yield_value(std::move(try_value)); \ - }) - -/// Drop in replacement for bool when C++ implcit conversions stand in your way -struct Explicit_Bool -{ - bool value; - - constexpr Explicit_Bool(bool b) : value(b) - { - } - - constexpr Explicit_Bool(auto &&) = delete; - - constexpr operator bool() const - { - return value; - } -}; - -/// All unicode related operations -namespace unicode -{ - inline namespace special_runes - { - [[maybe_unused]] constexpr u32 Rune_Error = 0xfffd; - [[maybe_unused]] constexpr u32 Rune_Self = 0x80; - [[maybe_unused]] constexpr u32 Max_Bytes = 4; - } - - /// is_digit returns true if `digit` is ASCII digit - bool is_digit(u32 digit); - - /// is_space return true if `space` is ASCII blank character - bool is_space(u32 space); - - /// is_letter returns true if `letter` is considered a letter by Unicode - bool is_letter(u32 letter); - - /// is_identifier returns true if `letter` is valid character for identifier. - /// - /// It's modifier by is_first_character flag to determine some character classes - /// allowance like numbers, which are only allowed NOT at the front of the identifier - enum class First_Character : bool { Yes = true, No = false }; - bool is_identifier(u32 letter, First_Character is_first_character); -} - -/// utf8 encoding and decoding -namespace utf8 -{ - using namespace unicode::special_runes; - - /// Decodes rune and returns remaining string - auto decode(std::string_view s) -> std::pair; - - /// Returns length of the first rune in the provided string - auto length(std::string_view s) -> usize; - - struct Print { u32 rune; }; -} - -std::ostream& operator<<(std::ostream& os, utf8::Print const& print); - -/// Lexical token representation for Musique language -struct Token -{ - /// Type of Token - enum class Type - { - Symbol, ///< like repeat or choose or chord - Keyword, ///< like true, false, nil - Operator, ///< like "+", "-", "++", "<" - Chord, ///< chord or single note literal, like "c125" - Numeric, ///< numeric literal (floating point or integer) - Parameter_Separator, ///< "|" separaters arguments from block body - Expression_Separator, ///< ";" separates expressions. Used mainly to separate calls, like `foo 1 2; bar 3 4` - Open_Block, ///< "[" delimits anonymous block of code (potentially a function) - Close_Block, ///< "]" delimits anonymous block of code (potentially a function) - Open_Paren, ///< "(" used in arithmetic or as function invocation sarrounding - Close_Paren ///< ")" used in arithmetic or as function invocation sarrounding - }; - - /// Type of token - Type type; - - /// Matched source code to the token type - std::string_view source; - - /// Location of encountered token - Location location; -}; - -static constexpr usize Keywords_Count = 5; -static constexpr usize Operators_Count = 17; - -std::string_view type_name(Token::Type type); - -/// Token debug printing -std::ostream& operator<<(std::ostream& os, Token const& tok); - -/// Token type debug printing -std::ostream& operator<<(std::ostream& os, Token::Type type); - -struct Lines -{ - static Lines the; - - /// Region of lines in files - std::unordered_map> lines; - - /// Add lines from file - void add_file(std::string filename, std::string_view source); - - /// Add single line into file (REPL usage) - void add_line(std::string const& filename, std::string_view source, unsigned line_number); - - /// Print selected region - void print(std::ostream& os, std::string const& file, unsigned first_line, unsigned last_line) const; -}; - -/// Explicit marker of the end of file -struct End_Of_File {}; - -/// Lexer takes source code and turns it into list of tokens -/// -/// It allows for creating sequence of tokens by using next_token() method. -/// On each call to next_token() when source is non empty token is lexed and -/// source is beeing advanced by removing matched token from string. -struct Lexer -{ - /// Source that is beeing lexed - std::string_view source; - - /// Location in source of the last rune - /// - /// Used only for rewinding - u32 last_rune_length = 0; - - /// Start of the token that is currently beeing matched - char const* token_start = nullptr; - - /// Bytes matched so far - usize token_length = 0; - - /// Location of the start of a token that is currently beeing matched - Location token_location{}; - - /// Current location of Lexer in source - Location location{}; - - /// Previous location of Lexer in source - /// - /// Used only for rewinding - Location prev_location{}; - - /// Try to tokenize next token. - auto next_token() -> Result>; - - /// Skip whitespace and comments from the beggining of the source - /// - /// Utility function for next_token() - void skip_whitespace_and_comments(); - - /// Finds next rune in source - auto peek() const -> u32; - - /// Finds next rune in source and returns it, advancing the string - auto consume() -> u32; - - /// For test beeing - /// callable, current rune is passed to test - /// integral, current rune is tested for equality with test - /// string, current rune is tested for beeing in it - /// otherwise, current rune is tested for beeing in test - /// - /// When testing above yields truth, current rune is consumed. - /// Returns if rune was consumed - auto consume_if(auto test) -> bool; - - /// Consume two runes with given tests otherwise backtrack - auto consume_if(auto first, auto second) -> bool; - - /// Goes back last rune - void rewind(); - - /// Marks begin of token - void start(); - - /// Marks end of token and returns it's matching source - std::string_view finish(); -}; - -/// Representation of a node in program tree -struct Ast -{ - /// Constructs binary operator - static Ast binary(Token, Ast lhs, Ast rhs); - - /// Constructs block - static Ast block(Location location, Ast seq = sequence({})); - - /// Constructs call expression - static Ast call(std::vector call); - - /// Constructs block with parameters - static Ast lambda(Location location, Ast seq = sequence({}), std::vector parameters = {}); - - /// Constructs constants, literals and variable identifiers - static Ast literal(Token); - - /// Constructs sequence of operations - static Ast sequence(std::vector call); - - /// Constructs variable declaration - static Ast variable_declaration(Location loc, std::vector lvalues, std::optional rvalue); - - /// Available ASt types - enum class Type - { - Binary, ///< Binary operator application like `1` + `2` - Block, ///< Block expressions like `[42; hello]` - Lambda, ///< Block expression beeing functions like `[i|i+1]` - Call, ///< Function call application like `print 42` - Literal, ///< Compile time known constant like `c` or `1` - Sequence, ///< Several expressions sequences like `42`, `42; 32` - Variable_Declaration, ///< Declaration of a variable with optional value assigment like `var x = 10` or `var y` - }; - - /// Type of AST node - Type type; - - /// Location that introduced this node - Location location; - - /// Associated token - Token token; - - /// Child nodes - std::vector arguments{}; -}; - -bool operator==(Ast const& lhs, Ast const& rhs); -std::ostream& operator<<(std::ostream& os, Ast::Type type); -std::ostream& operator<<(std::ostream& os, Ast const& tree); - -/// Pretty print program tree for debugging purposes -void dump(Ast const& ast, unsigned indent = 0); - -/// Source code to program tree converter -/// -/// Intended to be used by library user only by Parser::parse() static function. -struct Parser -{ - /// List of tokens yielded from source - std::vector tokens; - - /// Current token id (offset in tokens array) - unsigned token_id = 0; - - /// Parses whole source code producing Ast or Error - /// using Parser structure internally - static Result parse(std::string_view source, std::string_view filename, unsigned line_number = 0); - - /// Parse sequence, collection of expressions - Result parse_sequence(); - - /// Parse either infix expression or variable declaration - Result parse_expression(); - - /// Parse infix expression - Result parse_infix_expression(); - - /// Parse right hand size of infix expression - Result parse_rhs_of_infix_expression(Ast lhs); - - /// Parse either index expression or atomic expression - Result parse_index_expression(); - - /// Parse function call, literal etc - Result parse_atomic_expression(); - - /// Parse variable declaration - Result parse_variable_declaration(); - - /// Utility function for identifier parsing - Result parse_identifier_with_trailing_separators(); - - /// Utility function for identifier parsing - Result parse_identifier(); - - /// Peek current token - Result peek() const; - - /// Peek type of the current token - Result peek_type() const; - - /// Consume current token - Token consume(); - - /// Tests if current token has given type - bool expect(Token::Type type) const; - - /// Tests if current token has given type and source - bool expect(Token::Type type, std::string_view lexeme) const; - - // Tests if current token has given type and the next token has given type and source - bool expect(Token::Type t1, Token::Type t2, std::string_view lexeme_for_t2) const; -}; - -/// Number type supporting integer and fractional constants -/// -/// \invariant gcd(num, den) == 1, after any operation -struct Number -{ - /// Type that represents numerator and denominator values - using value_type = i64; - - value_type num = 0; ///< Numerator of a fraction beeing represented - value_type den = 1; ///< Denominator of a fraction beeing represented - - constexpr Number() = default; - constexpr Number(Number const&) = default; - constexpr Number(Number &&) = default; - constexpr Number& operator=(Number const&) = default; - constexpr Number& operator=(Number &&) = default; - - explicit Number(value_type v); ///< Creates Number as fraction v / 1 - Number(value_type num, value_type den); ///< Creates Number as fraction num / den - - auto as_int() const -> value_type; ///< Returns self as int - auto simplify() const -> Number; ///< Returns self, but with gcd(num, den) == 1 - void simplify_inplace(); ///< Update self, to have gcd(num, den) == 1 - - bool operator==(Number const&) const; - bool operator!=(Number const&) const; - std::strong_ordering operator<=>(Number const&) const; - - Number operator+(Number const& rhs) const; - Number& operator+=(Number const& rhs); - Number operator-(Number const& rhs) const; - Number& operator-=(Number const& rhs); - Number operator*(Number const& rhs) const; - Number& operator*=(Number const& rhs); - Result operator/(Number const& rhs) const; - Result operator%(Number const& rhs) const; - - Number floor() const; ///< Return number rounded down to nearest integer - Number ceil() const; ///< Return number rounded up to nearest integer - Number round() const; ///< Return number rounded to nearest integer - - Result inverse() const; ///< Return number raised to power -1 - Result pow(Number n) const; ///< Return number raised to power `n`. - - /// Parses source contained by token into a Number instance - static Result from(Token token); -}; - -std::ostream& operator<<(std::ostream& os, Number const& num); - -struct Env; -struct Interpreter; -struct Value; - -using Intrinsic = Result(*)(Interpreter &i, std::vector); - -/// Lazy Array / Continuation / Closure type thingy -struct Block -{ - /// Location of definition / creation - Location location; - - /// Names of expected parameters - std::vector parameters; - - /// Body that will be executed - Ast body; - - /// Context from which block was created. Used for closures - std::shared_ptr context; - - /// Calling block - Result operator()(Interpreter &i, std::vector params); - - /// Indexing block - Result index(Interpreter &i, unsigned position) const; - - /// Count of elements in block - usize size() const; -}; - -/// Representation of musical note or musical pause -struct Note -{ - /// Base of a note, like `c` (=0), `c#` (=1) `d` (=2) - /// Or nullopt where there is no note - case when we have pause - std::optional base = std::nullopt; - - /// Octave in MIDI acceptable range (from -1 to 9 inclusive) - std::optional octave = std::nullopt; - - /// Length of playing note - std::optional length = std::nullopt; - - /// Create Note from string - static std::optional from(std::string_view note); - - /// Extract midi note number - std::optional into_midi_note() const; - - /// Extract midi note number, but when octave is not present use provided default - u8 into_midi_note(i8 default_octave) const; - - bool operator==(Note const&) const; - - std::partial_ordering operator<=>(Note const&) const; - - /// Simplify note by adding base to octave if octave is present - void simplify_inplace(); -}; - -std::ostream& operator<<(std::ostream& os, Note note); - -/// Represantation of simultaneously played notes, aka musical chord -struct Chord -{ - std::vector notes; ///< Notes composing a chord - - /// Parse chord literal from provided source - static Chord from(std::string_view source); - - bool operator==(Chord const&) const = default; - - /// Fill length and octave or sequence multiple chords - Result operator()(Interpreter &i, std::vector args); -}; - -std::ostream& operator<<(std::ostream& os, Chord const& chord); - -/// Eager Array -struct Array -{ - /// Elements that are stored in array - std::vector elements; - - /// Index element of an array - Result index(Interpreter &i, unsigned position) const; - - /// Count of elements - usize size() const; - - bool operator==(Array const&) const = default; -}; - -std::ostream& operator<<(std::ostream& os, Array const& v); - -/// Representation of any value in language -struct Value -{ - /// Creates value from literal contained in Token - static Result from(Token t); - - /// Create value holding provided boolean - /// - /// Using Explicit_Bool to prevent from implicit casts - static Value from(Explicit_Bool b); - - static Value from(Array &&array); ///< Create value of type array holding provided array - static Value from(Block &&l); ///< Create value of type block holding provided block - static Value from(Chord chord); ///< Create value of type music holding provided chord - static Value from(Note n); ///< Create value of type music holding provided note - static Value from(Number n); ///< Create value of type number holding provided number - static Value from(char const* s); ///< Create value of type symbol holding provided symbol - static Value from(std::string s); ///< Create value of type symbol holding provided symbol - static Value from(std::string_view s); ///< Create value of type symbol holding provided symbol - static Value from(std::vector &&array); ///< Create value of type array holding provided array - - enum class Type - { - Nil, ///< Unit type, used for denoting emptiness and result of some side effect only functions - Bool, ///< Boolean type, used for logic computations - Number, ///< Number type, representing only rational numbers - Symbol, ///< Symbol type, used to represent identifiers - Intrinsic, ///< Intrinsic functions that are implemented in C++ - Block, ///< Block type, containing block value (lazy array/closure/lambda like) - Array, ///< Array type, eager array - Music, ///< Music type, - }; - - Value() = default; - Value(Value const&) = default; - Value(Value &&) = default; - Value& operator=(Value const&) = default; - Value& operator=(Value &&) = default; - - /// Contructs Intrinsic, used to simplify definition of intrinsics - inline Value(Intrinsic intr) : type{Type::Intrinsic}, intr(intr) - { - } - - Type type = Type::Nil; - bool b; - Number n; - Intrinsic intr; - Block blk; - Chord chord; - Array array; - - // TODO Most strings should not be allocated by Value, but reference to string allocated previously - // Wrapper for std::string is needed that will allocate only when needed, middle ground between: - // std::string - always owning string type - // std::string_view - not-owning string type - std::string s{}; - - /// Returns truth judgment for current type, used primarly for if function - bool truthy() const; - - /// Returns false judgment for current type, used primarly for if function - bool falsy() const; - - /// Calls contained value if it can be called - Result operator()(Interpreter &i, std::vector args); - - /// Index contained value if it can be called - Result index(Interpreter &i, unsigned position) const; - - /// Return elements count of contained value if it can be measured - usize size() const; - - bool operator==(Value const& other) const; - - std::partial_ordering operator<=>(Value const& other) const; -}; - -template -struct Member_For_Value_Type {}; - -template<> struct Member_For_Value_Type -{ static constexpr auto value = &Value::b; }; - -template<> struct Member_For_Value_Type -{ static constexpr auto value = &Value::n; }; - -template<> struct Member_For_Value_Type -{ static constexpr auto value = &Value::s; }; - -template<> struct Member_For_Value_Type -{ static constexpr auto value = &Value::intr; }; - -template<> struct Member_For_Value_Type -{ static constexpr auto value = &Value::blk; }; - -template<> struct Member_For_Value_Type -{ static constexpr auto value = &Value::array; }; - -template<> struct Member_For_Value_Type -{ static constexpr auto value = &Value::chord; }; - -/// Returns type name of Value type -std::string_view type_name(Value::Type t); - -std::ostream& operator<<(std::ostream& os, Value const& v); - -/// Collection holding all variables in given scope. -struct Env : std::enable_shared_from_this -{ - /// Constructor of Env class - static std::shared_ptr make(); - - /// Global scope that is beeing set by Interpreter - static std::shared_ptr global; - - /// Variables in current scope - std::unordered_map variables; - - /// Parent scope - std::shared_ptr parent; - - Env(Env const&) = delete; - Env(Env &&) = default; - Env& operator=(Env const&) = delete; - Env& operator=(Env &&) = default; - - /// Defines new variable regardless of it's current existance - Env& force_define(std::string name, Value new_value); - - /// Finds variable in current or parent scopes - Value* find(std::string const& name); - - /// Create new scope with self as parent - std::shared_ptr enter(); - - /// Leave current scope returning parent - std::shared_ptr leave(); - -private: - /// Ensure that all values of this class are behind shared_ptr - Env() = default; -}; - -/// Context holds default values for music related actions -struct Context -{ - /// Default note octave - i8 octave = 4; - - /// Default note length - Number length = Number(1, 4); - - /// Default BPM - unsigned bpm = 120; - - /// Fills empty places in Note like octave and length with default values from context - Note fill(Note) const; - - /// Converts length to seconds with current bpm - std::chrono::duration length_to_duration(std::optional length) const; -}; - -/// Given program tree evaluates it into Value -struct Interpreter -{ - /// MIDI connection that is used to play music. - /// It's optional for simple interpreter testing. - midi::Connection *midi_connection = nullptr; - - /// Operators defined for language - std::unordered_map operators; - - /// Current environment (current scope) - std::shared_ptr env; - - /// Context stack. `constext_stack.back()` is a current context. - /// There is always at least one context - std::vector context_stack; - - std::function(Interpreter&, Value)> default_action; - - struct Incoming_Midi_Callbacks; - std::unique_ptr callbacks; - void register_callbacks(); - - Interpreter(); - ~Interpreter(); - Interpreter(Interpreter const&) = delete; - Interpreter(Interpreter &&) = default; - - /// Try to evaluate given program tree - Result eval(Ast &&ast); - - // Enter scope by changing current environment - void enter_scope(); - - // Leave scope by changing current environment - void leave_scope(); - - /// Play note resolving any missing parameters with context via `midi_connection` member. - std::optional play(Chord); - - /// Add to global interpreter scope all builtin function definitions - /// - /// Invoked during construction - void register_builtin_functions(); - - /// Add to interpreter operators table all operators - /// - /// Invoked during construction - void register_builtin_operators(); -}; - -namespace errors -{ - [[noreturn]] - void all_tokens_were_not_parsed(std::span); -} - -/// Intrinsic implementation primitive providing a short way to check if arguments match required type signature -static inline bool typecheck(std::vector const& args, auto const& ...expected_types) -{ - return (args.size() == sizeof...(expected_types)) && - [&args, expected_types...](std::index_sequence) { - return ((expected_types == args[I].type) && ...); - } (std::make_index_sequence{}); -} - -/// Intrinsic implementation primitive providing a short way to move values based on matched type signature -static inline bool typecheck_front(std::vector const& args, auto const& ...expected_types) -{ - return (args.size() >= sizeof...(expected_types)) && - [&args, expected_types...](std::index_sequence) { - return ((expected_types == args[I].type) && ...); - } (std::make_index_sequence{}); -} - -/// Intrinsic implementation primitive providing a short way to move values based on matched type signature -template -static inline auto move_from(std::vector& args) -{ - return [&args](std::index_sequence) { - return std::tuple { (std::move(args[I]).*(Member_For_Value_Type::value)) ... }; - } (std::make_index_sequence{}); -} - -/// Shape abstraction to define what types are required once -template -struct Shape -{ - static inline auto move_from(std::vector& args) { return ::move_from(args); } - static inline auto typecheck(std::vector& args) { return ::typecheck(args, Types...); } - static inline auto typecheck_front(std::vector& args) { return ::typecheck_front(args, Types...); } - - static inline auto typecheck_and_move(std::vector& args) - { - return typecheck(args) ? std::optional { move_from(args) } : std::nullopt; - } -}; - -/// Returns if type can be indexed -static constexpr bool is_indexable(Value::Type type) -{ - return type == Value::Type::Array || type == Value::Type::Block; -} - -/// Returns if type can be called -static constexpr bool is_callable(Value::Type type) -{ - return type == Value::Type::Block || type == Value::Type::Intrinsic; -} - -/// Flattens one layer: `[[[1], 2], 3]` becomes `[[1], 2, 3]` -Result> flatten(Interpreter &i, std::span); -Result> flatten(Interpreter &i, std::vector); - -template<> struct std::hash { std::size_t operator()(Token const&) const; }; -template<> struct std::hash { std::size_t operator()(Ast const&) const; }; -template<> struct std::hash { std::size_t operator()(Number const&) const; }; -template<> struct std::hash { std::size_t operator()(Value const&) const; }; - -struct Value_Formatter -{ - enum Context - { - Free, - Inside_Block - }; - - Context context = Free; - unsigned indent = 0; - - Value_Formatter nest(Context nested = Free) const; - - std::optional format(std::ostream& os, Interpreter &interpreter, Value const& value); -}; - -Result format(Interpreter &i, Value const& value); - -#endif diff --git a/include/musique_internal.hh b/include/musique_internal.hh deleted file mode 100644 index da64188..0000000 --- a/include/musique_internal.hh +++ /dev/null @@ -1,148 +0,0 @@ -#ifndef Musique_Internal_HH -#define Musique_Internal_HH - -#include -#include -#include -#include - -/// Allows creation of guards that ensure proper type -template -struct Guard -{ - std::string_view name; - std::array possibilities; - errors::Unsupported_Types_For::Type type = errors::Unsupported_Types_For::Function; - - inline Error yield_error() const - { - auto error = errors::Unsupported_Types_For { - .type = type, - .name = std::string(name), - .possibilities = {} - }; - std::transform(possibilities.begin(), possibilities.end(), std::back_inserter(error.possibilities), [](auto s) { - return std::string(s); - }); - return Error { std::move(error) }; - } - - inline std::optional yield_result() const - { - return yield_error(); - } - - inline std::optional operator()(bool(*predicate)(Value::Type), Value const& v) const - { - return predicate(v.type) ? std::optional{} : yield_result(); - } -}; - -/// Binary operation may be vectorized when there are two argument which one is indexable and other is not -static inline bool may_be_vectorized(std::vector const& args) -{ - return args.size() == 2 && (is_indexable(args[0].type) != is_indexable(args[1].type)); -} - -struct Interpreter::Incoming_Midi_Callbacks -{ - Value note_on{}; - Value note_off{}; - - inline Incoming_Midi_Callbacks() = default; - - Incoming_Midi_Callbacks(Incoming_Midi_Callbacks &&) = delete; - Incoming_Midi_Callbacks(Incoming_Midi_Callbacks const&) = delete; - - Incoming_Midi_Callbacks& operator=(Incoming_Midi_Callbacks &&) = delete; - Incoming_Midi_Callbacks& operator=(Incoming_Midi_Callbacks const&) = delete; - - - inline void add_callbacks(midi::Connection &midi, Interpreter &interpreter) - { - register_callback(midi.note_on_callback, note_on, interpreter); - register_callback(midi.note_off_callback, note_off, interpreter); - } - - template - inline void register_callback(std::function &target, Value &callback, Interpreter &i) - { - if (&callback == ¬e_on || &callback == ¬e_off) { - // This messages have MIDI note number as second value, so they should be represented - // in our own note abstraction, not as numbers. - target = [interpreter = &i, callback = &callback](T ...source_args) - { - if (callback->type != Value::Type::Nil) { - std::vector args { Value::from(Number(source_args))... }; - args[1] = Value::from(Chord { .notes { Note { - .base = i32(args[1].n.num % 12), - .octave = args[1].n.num / 12 - }}}); - auto result = (*callback)(*interpreter, std::move(args)); - // We discard this since callback is running in another thread. - (void) result; - } - }; - } else { - // Generic case, preserve all passed parameters as numbers - target = [interpreter = &i, callback = &callback](T ...source_args) - { - if (callback->type != Value::Type::Nil) { - auto result = (*callback)(*interpreter, { Value::from(Number(source_args))... }); - // We discard this since callback is running in another thread. - (void) result; - } - }; - } - } -}; - -enum class Midi_Connection_Type { Output, Input }; -std::optional ensure_midi_connection_available(Interpreter&, Midi_Connection_Type, std::string_view operation_name); - -constexpr std::size_t hash_combine(std::size_t lhs, std::size_t rhs) { - return lhs ^= rhs + 0x9e3779b9 + (lhs << 6) + (lhs >> 2); -} - -template -Result wrap_value(Result &&value) -{ - return std::move(value).map([](auto &&value) { return Value::from(std::move(value)); }); -} - -Value wrap_value(auto &&value) -{ - return Value::from(std::move(value)); -} - -/// Generic algorithms support -namespace algo -{ - /// Check if predicate is true for all successive pairs of elements - constexpr bool pairwise_all( - std::ranges::forward_range auto &&range, - auto &&binary_predicate) - { - auto it = std::begin(range); - auto const end_it = std::end(range); - for (auto next_it = std::next(it); it != end_it && next_it != end_it; ++it, ++next_it) { - if (not binary_predicate(*it, *next_it)) { - return false; - } - } - return true; - } - - /// Fold that stops iteration on error value via Result type - template - constexpr Result fold(auto&& range, T init, auto &&reducer) - requires (is_template_v) - { - for (auto &&value : range) { - init = Try(reducer(std::move(init), value)); - } - return init; - } -} - -#endif diff --git a/musique/algo.hh b/musique/algo.hh new file mode 100644 index 0000000..02efd1c --- /dev/null +++ b/musique/algo.hh @@ -0,0 +1,43 @@ +#ifndef MUSIQUE_ALGO_HH +#define MUSIQUE_ALGO_HH + +#include +#include +#include +#include + +/// Generic algorithms support +namespace algo +{ + /// Check if predicate is true for all successive pairs of elements + constexpr bool pairwise_all( + std::ranges::forward_range auto &&range, + auto &&binary_predicate) + { + auto it = std::begin(range); + auto const end_it = std::end(range); + for (auto next_it = std::next(it); it != end_it && next_it != end_it; ++it, ++next_it) { + if (not binary_predicate(*it, *next_it)) { + return false; + } + } + return true; + } + + /// Fold that stops iteration on error value via Result type + template + constexpr Result fold(auto&& range, T init, auto &&reducer) + requires (is_template_v) + { + for (auto &&value : range) { + init = Try(reducer(std::move(init), value)); + } + return init; + } +} + +/// Flattens one layer: `[[[1], 2], 3]` becomes `[[1], 2, 3]` +Result> flatten(Interpreter &i, std::span); +Result> flatten(Interpreter &i, std::vector); + +#endif diff --git a/musique/array.hh b/musique/array.hh new file mode 100644 index 0000000..66130b7 --- /dev/null +++ b/musique/array.hh @@ -0,0 +1,27 @@ +#ifndef MUSIQUE_ARRAY_HH +#define MUSIQUE_ARRAY_HH + +#include +#include "result.hh" + +struct Interpreter; +struct Value; + +/// Eager Array +struct Array +{ + /// Elements that are stored in array + std::vector elements; + + /// Index element of an array + Result index(Interpreter &i, unsigned position) const; + + /// Count of elements + usize size() const; + + bool operator==(Array const&) const = default; +}; + +std::ostream& operator<<(std::ostream& os, Array const& v); + +#endif diff --git a/musique/ast.hh b/musique/ast.hh new file mode 100644 index 0000000..a18d224 --- /dev/null +++ b/musique/ast.hh @@ -0,0 +1,66 @@ +#ifndef MUSIQUE_AST_HH +#define MUSIQUE_AST_HH + +#include "token.hh" +#include +#include + +/// Representation of a node in program tree +struct Ast +{ + /// Constructs binary operator + static Ast binary(Token, Ast lhs, Ast rhs); + + /// Constructs block + static Ast block(Location location, Ast seq = sequence({})); + + /// Constructs call expression + static Ast call(std::vector call); + + /// Constructs block with parameters + static Ast lambda(Location location, Ast seq = sequence({}), std::vector parameters = {}); + + /// Constructs constants, literals and variable identifiers + static Ast literal(Token); + + /// Constructs sequence of operations + static Ast sequence(std::vector call); + + /// Constructs variable declaration + static Ast variable_declaration(Location loc, std::vector lvalues, std::optional rvalue); + + /// Available ASt types + enum class Type + { + Binary, ///< Binary operator application like `1` + `2` + Block, ///< Block expressions like `[42; hello]` + Lambda, ///< Block expression beeing functions like `[i|i+1]` + Call, ///< Function call application like `print 42` + Literal, ///< Compile time known constant like `c` or `1` + Sequence, ///< Several expressions sequences like `42`, `42; 32` + Variable_Declaration, ///< Declaration of a variable with optional value assigment like `var x = 10` or `var y` + }; + + /// Type of AST node + Type type; + + /// Location that introduced this node + Location location; + + /// Associated token + Token token; + + /// Child nodes + std::vector arguments{}; +}; + +bool operator==(Ast const& lhs, Ast const& rhs); +std::ostream& operator<<(std::ostream& os, Ast::Type type); +std::ostream& operator<<(std::ostream& os, Ast const& tree); + +/// Pretty print program tree for debugging purposes +void dump(Ast const& ast, unsigned indent = 0); + +template<> struct std::hash { std::size_t operator()(Ast const&) const; }; + +#endif diff --git a/musique/block.hh b/musique/block.hh new file mode 100644 index 0000000..20a8a2c --- /dev/null +++ b/musique/block.hh @@ -0,0 +1,40 @@ +#ifndef MUSIQUE_BLOCK_HH +#define MUSIQUE_BLOCK_HH + +#include "result.hh" +#include "ast.hh" + +#include + +struct Env; +struct Interpreter; +struct Value; + +using Intrinsic = Result(*)(Interpreter &i, std::vector); + +/// Lazy Array / Continuation / Closure type thingy +struct Block +{ + /// Location of definition / creation + Location location; + + /// Names of expected parameters + std::vector parameters; + + /// Body that will be executed + Ast body; + + /// Context from which block was created. Used for closures + std::shared_ptr context; + + /// Calling block + Result operator()(Interpreter &i, std::vector params); + + /// Indexing block + Result index(Interpreter &i, unsigned position) const; + + /// Count of elements in block + usize size() const; +}; + +#endif diff --git a/src/builtin_functions.cc b/musique/builtin_functions.cc similarity index 99% rename from src/builtin_functions.cc rename to musique/builtin_functions.cc index c575f40..ec0630f 100644 --- a/src/builtin_functions.cc +++ b/musique/builtin_functions.cc @@ -1,5 +1,10 @@ -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/src/builtin_operators.cc b/musique/builtin_operators.cc similarity index 98% rename from src/builtin_operators.cc rename to musique/builtin_operators.cc index 752af67..b771515 100644 --- a/src/builtin_operators.cc +++ b/musique/builtin_operators.cc @@ -1,6 +1,9 @@ -#include -#include #include +#include +#include +#include +#include +#include /// Intrinsic implementation primitive to ease operation vectorization static Result vectorize(auto &&operation, Interpreter &interpreter, Value lhs, Value rhs) diff --git a/musique/chord.hh b/musique/chord.hh new file mode 100644 index 0000000..abd902c --- /dev/null +++ b/musique/chord.hh @@ -0,0 +1,22 @@ +#include + +#include "note.hh" + +struct Interpreter; +struct Value; + +/// Represantation of simultaneously played notes, aka musical chord +struct Chord +{ + std::vector notes; ///< Notes composing a chord + + /// Parse chord literal from provided source + static Chord from(std::string_view source); + + bool operator==(Chord const&) const = default; + + /// Fill length and octave or sequence multiple chords + Result operator()(Interpreter &i, std::vector args); +}; + +std::ostream& operator<<(std::ostream& os, Chord const& chord); diff --git a/musique/common.hh b/musique/common.hh new file mode 100644 index 0000000..c9fd875 --- /dev/null +++ b/musique/common.hh @@ -0,0 +1,52 @@ +#ifndef MUSIQUE_COMMON_HH +#define MUSIQUE_COMMON_HH + +#include +#include +#include + +using namespace std::string_literals; +using namespace std::string_view_literals; + +using u8 = std::uint8_t; +using u16 = std::uint16_t; +using u32 = std::uint32_t; +using u64 = std::uint64_t; + +using i8 = std::int8_t; +using i16 = std::int16_t; +using i32 = std::int32_t; +using i64 = std::int64_t; + +using usize = std::size_t; +using isize = std::ptrdiff_t; + +/// Combine several lambdas into one for visiting std::variant +template +struct Overloaded : Lambdas... { using Lambdas::operator()...; }; + +/// Returns if provided thingy is a given template +template typename Template, typename> +struct is_template : std::false_type {}; + +template typename Template, typename ...T> +struct is_template> : std::true_type {}; + +/// Returns if provided thingy is a given template +template typename Template, typename T> +constexpr auto is_template_v = is_template::value; + +/// Drop in replacement for bool when C++ impilcit conversions stand in your way +struct Explicit_Bool +{ + bool value; + constexpr Explicit_Bool(bool b) : value(b) { } + constexpr Explicit_Bool(auto &&) = delete; + constexpr operator bool() const { return value; } +}; + +constexpr std::size_t hash_combine(std::size_t lhs, std::size_t rhs) { + return lhs ^= rhs + 0x9e3779b9 + (lhs << 6) + (lhs >> 2); +} + +#endif diff --git a/src/context.cc b/musique/context.cc similarity index 92% rename from src/context.cc rename to musique/context.cc index 0ba805c..b64c259 100644 --- a/src/context.cc +++ b/musique/context.cc @@ -1,4 +1,4 @@ -#include +#include Note Context::fill(Note note) const { diff --git a/musique/context.hh b/musique/context.hh new file mode 100644 index 0000000..68dbdf0 --- /dev/null +++ b/musique/context.hh @@ -0,0 +1,28 @@ +#ifndef MUSIQUE_CONTEXT_HH +#define MUSIQUE_CONTEXT_HH + +#include "common.hh" +#include "note.hh" +#include "number.hh" +#include + +/// Context holds default values for music related actions +struct Context +{ + /// Default note octave + i8 octave = 4; + + /// Default note length + Number length = Number(1, 4); + + /// Default BPM + unsigned bpm = 120; + + /// Fills empty places in Note like octave and length with default values from context + Note fill(Note) const; + + /// Converts length to seconds with current bpm + std::chrono::duration length_to_duration(std::optional length) const; +}; + +#endif diff --git a/src/environment.cc b/musique/env.cc similarity index 96% rename from src/environment.cc rename to musique/env.cc index 969c93c..b82702d 100644 --- a/src/environment.cc +++ b/musique/env.cc @@ -1,4 +1,4 @@ -#include +#include #include diff --git a/musique/env.hh b/musique/env.hh new file mode 100644 index 0000000..f3e4b76 --- /dev/null +++ b/musique/env.hh @@ -0,0 +1,45 @@ +#ifndef MUSIQUE_ENV_HH +#define MUSIQUE_ENV_HH + +#include +#include +#include "value.hh" + +/// Collection holding all variables in given scope. +struct Env : std::enable_shared_from_this +{ + /// Constructor of Env class + static std::shared_ptr make(); + + /// Global scope that is beeing set by Interpreter + static std::shared_ptr global; + + /// Variables in current scope + std::unordered_map variables; + + /// Parent scope + std::shared_ptr parent; + + Env(Env const&) = delete; + Env(Env &&) = default; + Env& operator=(Env const&) = delete; + Env& operator=(Env &&) = default; + + /// Defines new variable regardless of it's current existance + Env& force_define(std::string name, Value new_value); + + /// Finds variable in current or parent scopes + Value* find(std::string const& name); + + /// Create new scope with self as parent + std::shared_ptr enter(); + + /// Leave current scope returning parent + std::shared_ptr leave(); + +private: + /// Ensure that all values of this class are behind shared_ptr + Env() = default; +}; + +#endif diff --git a/src/errors.cc b/musique/errors.cc similarity index 99% rename from src/errors.cc rename to musique/errors.cc index 72b17e0..14ed3cc 100644 --- a/src/errors.cc +++ b/musique/errors.cc @@ -1,4 +1,8 @@ -#include +#include +#include +#include +#include +#include #include #include diff --git a/musique/errors.hh b/musique/errors.hh new file mode 100644 index 0000000..9036e55 --- /dev/null +++ b/musique/errors.hh @@ -0,0 +1,222 @@ +#ifndef MUSIQUE_ERRORS_HH +#define MUSIQUE_ERRORS_HH + +#if defined(__cpp_lib_source_location) +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include + +// To make sure, that we don't collide with macro +#ifdef assert +#undef assert +#endif + +/// Guards that program exits if condition does not hold +void assert(bool condition, std::string message, Location loc = Location::caller()); + +/// Marks part of code that was not implemented yet +[[noreturn]] void unimplemented(std::string_view message = {}, Location loc = Location::caller()); + +/// Marks location that should not be reached +[[noreturn]] void unreachable(Location loc = Location::caller()); + +/// Error handling related functions and definitions +namespace errors +{ + /// When user puts emoji in the source code + struct Unrecognized_Character + { + u32 invalid_character; + }; + + /// When parser was expecting code but encountered end of file + struct Unexpected_Empty_Source + { + enum { Block_Without_Closing_Bracket } reason; + std::optional start; + }; + + /// When user passed numeric literal too big for numeric type + struct Failed_Numeric_Parsing + { + std::errc reason; + }; + + /// When user forgot semicolon or brackets + struct Expected_Expression_Separator_Before + { + std::string_view what; + }; + + /// When some keywords are not allowed in given context + struct Unexpected_Keyword + { + std::string_view keyword; + }; + + /// When user tried to use operator that was not defined + struct Undefined_Operator + { + std::string_view op; + }; + + /// When user tries to use operator with wrong arity of arguments + struct Wrong_Arity_Of + { + /// Type of operation + enum Type { Operator, Function } type; + + /// Name of operation + std::string_view name; + + /// Arity that was expected by given operation + size_t expected_arity; + + /// Arit that user provided + size_t actual_arity; + }; + + /// When user tried to call something that can't be called + struct Not_Callable + { + std::string_view type; + }; + + /// When user provides literal where identifier should be + struct Literal_As_Identifier + { + std::string_view type_name; + std::string_view source; + std::string_view context; + }; + + /// When user provides wrong type for given operation + struct Unsupported_Types_For + { + /// Type of operation + enum Type { Operator, Function } type; + + /// Name of operation + std::string_view name; + + /// Possible ways to use it correctly + std::vector possibilities; + }; + + /// When user tries to use variable that has not been defined yet. + struct Missing_Variable + { + /// Name of variable + std::string name; + }; + + /// When user tries to invoke some MIDI action but haven't established MIDI connection + struct Operation_Requires_Midi_Connection + { + /// If its input or output connection missing + bool is_input; + + /// Name of the operation that was beeing invoked + std::string name; + }; + + /// When user tries to get element from collection with index higher then collection size + struct Out_Of_Range + { + /// Index that was required by the user + size_t required_index; + + /// Size of accessed collection + size_t size; + }; + + struct Closing_Token_Without_Opening + { + enum { + Block = ']', + Paren = ')' + } type; + }; + + struct Arithmetic + { + enum Type + { + Division_By_Zero, + Fractional_Modulo, + Unable_To_Calculate_Modular_Multiplicative_Inverse + } type; + }; + + /// Collection of messages that are considered internal and should not be printed to the end user. + namespace internal + { + /// When encountered token that was supposed to be matched in higher branch of the parser + struct Unexpected_Token + { + /// Type of the token + std::string_view type; + + /// Source of the token + std::string_view source; + + /// Where this token was encountered that was unexpected? + std::string_view when; + }; + } + + /// All possible error types + using Details = std::variant< + Arithmetic, + Closing_Token_Without_Opening, + Expected_Expression_Separator_Before, + Failed_Numeric_Parsing, + Literal_As_Identifier, + Missing_Variable, + Not_Callable, + Operation_Requires_Midi_Connection, + Out_Of_Range, + Undefined_Operator, + Unexpected_Empty_Source, + Unexpected_Keyword, + Unrecognized_Character, + Unsupported_Types_For, + Wrong_Arity_Of, + internal::Unexpected_Token + >; +} + +/// Represents all recoverable error messages that interpreter can produce +struct Error +{ + /// Specific message details + errors::Details details; + + /// Location that coused all this trouble + std::optional location = std::nullopt; + + /// Return self with new location + Error with(Location) &&; +}; + +/// Error pretty printing +std::ostream& operator<<(std::ostream& os, Error const& err); + +struct Token; + +namespace errors +{ + [[noreturn]] + void all_tokens_were_not_parsed(std::span); +} + +#endif // MUSIQUE_ERRORS_HH diff --git a/src/format.cc b/musique/format.cc similarity index 93% rename from src/format.cc rename to musique/format.cc index 4af2118..94a0b66 100644 --- a/src/format.cc +++ b/musique/format.cc @@ -1,4 +1,7 @@ -#include +#include +#include +#include +#include #include Result format(Interpreter &i, Value const& value) diff --git a/musique/format.hh b/musique/format.hh new file mode 100644 index 0000000..53a4bd3 --- /dev/null +++ b/musique/format.hh @@ -0,0 +1,27 @@ +#ifndef MUSIQUE_FORMAT_HH +#define MUSIQUE_FORMAT_HH + +#include "result.hh" + +struct Interpreter; +struct Value; + +struct Value_Formatter +{ + enum Context + { + Free, + Inside_Block + }; + + Context context = Free; + unsigned indent = 0; + + Value_Formatter nest(Context nested = Free) const; + + std::optional format(std::ostream& os, Interpreter &interpreter, Value const& value); +}; + +Result format(Interpreter &i, Value const& value); + +#endif diff --git a/musique/guard.hh b/musique/guard.hh new file mode 100644 index 0000000..a5ef619 --- /dev/null +++ b/musique/guard.hh @@ -0,0 +1,42 @@ +#ifndef MUSIQUE_GUARD_HH +#define MUSIQUE_GUARD_HH + +#include +#include +#include +#include +#include + +/// Allows creation of guards that ensure proper type +template +struct Guard +{ + std::string_view name; + std::array possibilities; + errors::Unsupported_Types_For::Type type = errors::Unsupported_Types_For::Function; + + inline Error yield_error() const + { + auto error = errors::Unsupported_Types_For { + .type = type, + .name = std::string(name), + .possibilities = {} + }; + std::transform(possibilities.begin(), possibilities.end(), std::back_inserter(error.possibilities), [](auto s) { + return std::string(s); + }); + return Error { std::move(error) }; + } + + inline std::optional yield_result() const + { + return yield_error(); + } + + inline std::optional operator()(bool(*predicate)(Value::Type), Value const& v) const + { + return predicate(v.type) ? std::optional{} : yield_result(); + } +}; + +#endif diff --git a/musique/incoming_midi.hh b/musique/incoming_midi.hh new file mode 100644 index 0000000..e14dcc3 --- /dev/null +++ b/musique/incoming_midi.hh @@ -0,0 +1,60 @@ +#ifndef MUSIQUE_INCOMING_MIDI +#define MUSIQUE_INCOMING_MIDI + +#include "interpreter.hh" + +struct Interpreter::Incoming_Midi_Callbacks +{ + Value note_on{}; + Value note_off{}; + + inline Incoming_Midi_Callbacks() = default; + + Incoming_Midi_Callbacks(Incoming_Midi_Callbacks &&) = delete; + Incoming_Midi_Callbacks(Incoming_Midi_Callbacks const&) = delete; + + Incoming_Midi_Callbacks& operator=(Incoming_Midi_Callbacks &&) = delete; + Incoming_Midi_Callbacks& operator=(Incoming_Midi_Callbacks const&) = delete; + + + inline void add_callbacks(midi::Connection &midi, Interpreter &interpreter) + { + register_callback(midi.note_on_callback, note_on, interpreter); + register_callback(midi.note_off_callback, note_off, interpreter); + } + + template + inline void register_callback(std::function &target, Value &callback, Interpreter &i) + { + if (&callback == ¬e_on || &callback == ¬e_off) { + // This messages have MIDI note number as second value, so they should be represented + // in our own note abstraction, not as numbers. + target = [interpreter = &i, callback = &callback](T ...source_args) + { + if (callback->type != Value::Type::Nil) { + std::vector args { Value::from(Number(source_args))... }; + args[1] = Value::from(Chord { .notes { Note { + .base = i32(args[1].n.num % 12), + .octave = args[1].n.num / 12 + }}}); + auto result = (*callback)(*interpreter, std::move(args)); + // We discard this since callback is running in another thread. + (void) result; + } + }; + } else { + // Generic case, preserve all passed parameters as numbers + target = [interpreter = &i, callback = &callback](T ...source_args) + { + if (callback->type != Value::Type::Nil) { + auto result = (*callback)(*interpreter, { Value::from(Number(source_args))... }); + // We discard this since callback is running in another thread. + (void) result; + } + }; + } + } +}; + + +#endif diff --git a/src/interpreter.cc b/musique/interpreter.cc similarity index 98% rename from src/interpreter.cc rename to musique/interpreter.cc index 85278d9..dc4beeb 100644 --- a/src/interpreter.cc +++ b/musique/interpreter.cc @@ -1,5 +1,7 @@ -#include -#include +#include +#include +#include +#include #include #include diff --git a/musique/interpreter.hh b/musique/interpreter.hh new file mode 100644 index 0000000..f6750cf --- /dev/null +++ b/musique/interpreter.hh @@ -0,0 +1,62 @@ +#ifndef MUSIQUE_INTERPRETER_HH +#define MUSIQUE_INTERPRETER_HH + +#include +#include +#include + +/// Given program tree evaluates it into Value +struct Interpreter +{ + /// MIDI connection that is used to play music. + /// It's optional for simple interpreter testing. + midi::Connection *midi_connection = nullptr; + + /// Operators defined for language + std::unordered_map operators; + + /// Current environment (current scope) + std::shared_ptr env; + + /// Context stack. `constext_stack.back()` is a current context. + /// There is always at least one context + std::vector context_stack; + + std::function(Interpreter&, Value)> default_action; + + struct Incoming_Midi_Callbacks; + std::unique_ptr callbacks; + void register_callbacks(); + + Interpreter(); + ~Interpreter(); + Interpreter(Interpreter const&) = delete; + Interpreter(Interpreter &&) = default; + + /// Try to evaluate given program tree + Result eval(Ast &&ast); + + // Enter scope by changing current environment + void enter_scope(); + + // Leave scope by changing current environment + void leave_scope(); + + /// Play note resolving any missing parameters with context via `midi_connection` member. + std::optional play(Chord); + + /// Add to global interpreter scope all builtin function definitions + /// + /// Invoked during construction + void register_builtin_functions(); + + /// Add to interpreter operators table all operators + /// + /// Invoked during construction + void register_builtin_operators(); +}; + +enum class Midi_Connection_Type { Output, Input }; +std::optional ensure_midi_connection_available(Interpreter&, Midi_Connection_Type, std::string_view operation_name); + +#endif diff --git a/src/lexer.cc b/musique/lexer.cc similarity index 99% rename from src/lexer.cc rename to musique/lexer.cc index e3c9243..414a5a7 100644 --- a/src/lexer.cc +++ b/musique/lexer.cc @@ -1,7 +1,8 @@ -#include -#include +#include +#include #include +#include constexpr std::string_view Notes_Symbols = "abcedefghp"; constexpr std::string_view Valid_Operator_Chars = diff --git a/musique/lexer.hh b/musique/lexer.hh new file mode 100644 index 0000000..37c779b --- /dev/null +++ b/musique/lexer.hh @@ -0,0 +1,81 @@ +#ifndef MUSIQUE_LEXER_HH +#define MUSIQUE_LEXER_HH + +#include +#include +#include +#include + +/// Explicit marker of the end of file +struct End_Of_File {}; + +/// Lexer takes source code and turns it into list of tokens +/// +/// It allows for creating sequence of tokens by using next_token() method. +/// On each call to next_token() when source is non empty token is lexed and +/// source is beeing advanced by removing matched token from string. +struct Lexer +{ + /// Source that is beeing lexed + std::string_view source; + + /// Location in source of the last rune + /// + /// Used only for rewinding + u32 last_rune_length = 0; + + /// Start of the token that is currently beeing matched + char const* token_start = nullptr; + + /// Bytes matched so far + usize token_length = 0; + + /// Location of the start of a token that is currently beeing matched + Location token_location{}; + + /// Current location of Lexer in source + Location location{}; + + /// Previous location of Lexer in source + /// + /// Used only for rewinding + Location prev_location{}; + + /// Try to tokenize next token. + auto next_token() -> Result>; + + /// Skip whitespace and comments from the beggining of the source + /// + /// Utility function for next_token() + void skip_whitespace_and_comments(); + + /// Finds next rune in source + auto peek() const -> u32; + + /// Finds next rune in source and returns it, advancing the string + auto consume() -> u32; + + /// For test beeing + /// callable, current rune is passed to test + /// integral, current rune is tested for equality with test + /// string, current rune is tested for beeing in it + /// otherwise, current rune is tested for beeing in test + /// + /// When testing above yields truth, current rune is consumed. + /// Returns if rune was consumed + auto consume_if(auto test) -> bool; + + /// Consume two runes with given tests otherwise backtrack + auto consume_if(auto first, auto second) -> bool; + + /// Goes back last rune + void rewind(); + + /// Marks begin of token + void start(); + + /// Marks end of token and returns it's matching source + std::string_view finish(); +}; + +#endif diff --git a/src/lines.cc b/musique/lines.cc similarity index 94% rename from src/lines.cc rename to musique/lines.cc index d6d74d4..ca1c2fd 100644 --- a/src/lines.cc +++ b/musique/lines.cc @@ -1,6 +1,6 @@ -#include - #include +#include +#include Lines Lines::the; diff --git a/musique/lines.hh b/musique/lines.hh new file mode 100644 index 0000000..cb679e0 --- /dev/null +++ b/musique/lines.hh @@ -0,0 +1,25 @@ +#ifndef MUSIQUE_LINES_HH +#define MUSIQUE_LINES_HH + +#include +#include +#include + +struct Lines +{ + static Lines the; + + /// Region of lines in files + std::unordered_map> lines; + + /// Add lines from file + void add_file(std::string filename, std::string_view source); + + /// Add single line into file (REPL usage) + void add_line(std::string const& filename, std::string_view source, unsigned line_number); + + /// Print selected region + void print(std::ostream& os, std::string const& file, unsigned first_line, unsigned last_line) const; +}; + +#endif diff --git a/src/location.cc b/musique/location.cc similarity index 96% rename from src/location.cc rename to musique/location.cc index 122990b..2f50fe7 100644 --- a/src/location.cc +++ b/musique/location.cc @@ -1,4 +1,4 @@ -#include +#include Location Location::at(usize line, usize column) { diff --git a/musique/location.hh b/musique/location.hh new file mode 100644 index 0000000..173bfc0 --- /dev/null +++ b/musique/location.hh @@ -0,0 +1,48 @@ +#ifndef MUSIQUE_LOCATION_HH +#define MUSIQUE_LOCATION_HH + +#include +#include + +/// \brief Location describes code position in `file line column` format. +/// It's used both to represent position in source files provided +// to interpreter and internal interpreter usage. +struct Location +{ + std::string_view filename = ""; ///< File that location is pointing to + usize line = 1; ///< Line number (1 based) that location is pointing to + usize column = 1; ///< Column number (1 based) that location is pointing to + + /// Advances line and column numbers based on provided rune + /// + /// If rune is newline, then column is reset to 1, and line number is incremented. + /// Otherwise column number is incremented. + /// + /// @param rune Rune from which column and line numbers advancements are made. + Location& advance(u32 rune); + + bool operator==(Location const& rhs) const = default; + + //! Creates location at default filename with specified line and column number + static Location at(usize line, usize column); + + // Used to describe location of function call in interpreter (internal use only) +#if defined(__cpp_lib_source_location) + static Location caller(std::source_location loc = std::source_location::current()); +#elif (__has_builtin(__builtin_FILE) and __has_builtin(__builtin_LINE)) + static Location caller(char const* file = __builtin_FILE(), usize line = __builtin_LINE()); +#else +#error Cannot implement Location::caller function + /// Returns location of call in interpreter source code. + /// + /// Example of reporting where `foo()` was beeing called: + /// @code + /// void foo(Location loc = Location::caller()) { std::cout << loc << '\n'; } + /// @endcode + static Location caller(); +#endif +}; + +std::ostream& operator<<(std::ostream& os, Location const& location); + +#endif diff --git a/src/main.cc b/musique/main.cc similarity index 97% rename from src/main.cc rename to musique/main.cc index 6798bb3..5c4631c 100644 --- a/src/main.cc +++ b/musique/main.cc @@ -6,8 +6,15 @@ #include #include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include extern "C" { #include diff --git a/musique/note.hh b/musique/note.hh new file mode 100644 index 0000000..adb6124 --- /dev/null +++ b/musique/note.hh @@ -0,0 +1,40 @@ +#ifndef MUSIQUE_NOTE_HH +#define MUSIQUE_NOTE_HH + +#include + +#include "number.hh" + +/// Representation of musical note or musical pause +struct Note +{ + /// Base of a note, like `c` (=0), `c#` (=1) `d` (=2) + /// Or nullopt where there is no note - case when we have pause + std::optional base = std::nullopt; + + /// Octave in MIDI acceptable range (from -1 to 9 inclusive) + std::optional octave = std::nullopt; + + /// Length of playing note + std::optional length = std::nullopt; + + /// Create Note from string + static std::optional from(std::string_view note); + + /// Extract midi note number + std::optional into_midi_note() const; + + /// Extract midi note number, but when octave is not present use provided default + u8 into_midi_note(i8 default_octave) const; + + bool operator==(Note const&) const; + + std::partial_ordering operator<=>(Note const&) const; + + /// Simplify note by adding base to octave if octave is present + void simplify_inplace(); +}; + +std::ostream& operator<<(std::ostream& os, Note note); + +#endif diff --git a/src/number.cc b/musique/number.cc similarity index 99% rename from src/number.cc rename to musique/number.cc index 2ea1361..94adf0d 100644 --- a/src/number.cc +++ b/musique/number.cc @@ -1,5 +1,5 @@ -#include -#include +#include +#include #include #include diff --git a/musique/number.hh b/musique/number.hh new file mode 100644 index 0000000..dd8ab58 --- /dev/null +++ b/musique/number.hh @@ -0,0 +1,61 @@ +#ifndef MUSIQUE_NUMBER_HH +#define MUSIQUE_NUMBER_HH + +#include +#include +#include +#include + +/// Number type supporting integer and fractional constants +/// +/// \invariant gcd(num, den) == 1, after any operation +struct Number +{ + /// Type that represents numerator and denominator values + using value_type = i64; + + value_type num = 0; ///< Numerator of a fraction beeing represented + value_type den = 1; ///< Denominator of a fraction beeing represented + + constexpr Number() = default; + constexpr Number(Number const&) = default; + constexpr Number(Number &&) = default; + constexpr Number& operator=(Number const&) = default; + constexpr Number& operator=(Number &&) = default; + + explicit Number(value_type v); ///< Creates Number as fraction v / 1 + Number(value_type num, value_type den); ///< Creates Number as fraction num / den + + auto as_int() const -> value_type; ///< Returns self as int + auto simplify() const -> Number; ///< Returns self, but with gcd(num, den) == 1 + void simplify_inplace(); ///< Update self, to have gcd(num, den) == 1 + + bool operator==(Number const&) const; + bool operator!=(Number const&) const; + std::strong_ordering operator<=>(Number const&) const; + + Number operator+(Number const& rhs) const; + Number& operator+=(Number const& rhs); + Number operator-(Number const& rhs) const; + Number& operator-=(Number const& rhs); + Number operator*(Number const& rhs) const; + Number& operator*=(Number const& rhs); + Result operator/(Number const& rhs) const; + Result operator%(Number const& rhs) const; + + Number floor() const; ///< Return number rounded down to nearest integer + Number ceil() const; ///< Return number rounded up to nearest integer + Number round() const; ///< Return number rounded to nearest integer + + Result inverse() const; ///< Return number raised to power -1 + Result pow(Number n) const; ///< Return number raised to power `n`. + + /// Parses source contained by token into a Number instance + static Result from(Token token); +}; + +std::ostream& operator<<(std::ostream& os, Number const& num); + +template<> struct std::hash { std::size_t operator()(Number const&) const; }; + +#endif diff --git a/src/parser.cc b/musique/parser.cc similarity index 99% rename from src/parser.cc rename to musique/parser.cc index bb4aa91..95da490 100644 --- a/src/parser.cc +++ b/musique/parser.cc @@ -1,5 +1,6 @@ -#include -#include +#include +#include +#include #include #include diff --git a/musique/parser.hh b/musique/parser.hh new file mode 100644 index 0000000..5b907fa --- /dev/null +++ b/musique/parser.hh @@ -0,0 +1,68 @@ +#ifndef MUSIQUE_PARSER_HH +#define MUSIQUE_PARSER_HH + +#include "ast.hh" +#include "result.hh" + +/// Source code to program tree converter +/// +/// Intended to be used by library user only by Parser::parse() static function. +struct Parser +{ + /// List of tokens yielded from source + std::vector tokens; + + /// Current token id (offset in tokens array) + unsigned token_id = 0; + + /// Parses whole source code producing Ast or Error + /// using Parser structure internally + static Result parse(std::string_view source, std::string_view filename, unsigned line_number = 0); + + /// Parse sequence, collection of expressions + Result parse_sequence(); + + /// Parse either infix expression or variable declaration + Result parse_expression(); + + /// Parse infix expression + Result parse_infix_expression(); + + /// Parse right hand size of infix expression + Result parse_rhs_of_infix_expression(Ast lhs); + + /// Parse either index expression or atomic expression + Result parse_index_expression(); + + /// Parse function call, literal etc + Result parse_atomic_expression(); + + /// Parse variable declaration + Result parse_variable_declaration(); + + /// Utility function for identifier parsing + Result parse_identifier_with_trailing_separators(); + + /// Utility function for identifier parsing + Result parse_identifier(); + + /// Peek current token + Result peek() const; + + /// Peek type of the current token + Result peek_type() const; + + /// Consume current token + Token consume(); + + /// Tests if current token has given type + bool expect(Token::Type type) const; + + /// Tests if current token has given type and source + bool expect(Token::Type type, std::string_view lexeme) const; + + // Tests if current token has given type and the next token has given type and source + bool expect(Token::Type t1, Token::Type t2, std::string_view lexeme_for_t2) const; +}; + +#endif diff --git a/src/pretty.cc b/musique/pretty.cc similarity index 96% rename from src/pretty.cc rename to musique/pretty.cc index b18369d..5036e82 100644 --- a/src/pretty.cc +++ b/musique/pretty.cc @@ -1,4 +1,4 @@ -#include +#include namespace starters { diff --git a/musique/pretty.hh b/musique/pretty.hh new file mode 100644 index 0000000..bda4be4 --- /dev/null +++ b/musique/pretty.hh @@ -0,0 +1,28 @@ +#ifndef MUSIQUE_PRETTY_HH +#define MUSIQUE_PRETTY_HH + +#include + +/// All code related to pretty printing. Default mode is no_color +namespace pretty +{ + /// Mark start of printing an error + std::ostream& begin_error(std::ostream&); + + /// Mark start of printing a path + std::ostream& begin_path(std::ostream&); + + /// Mark start of printing a comment + std::ostream& begin_comment(std::ostream&); + + /// Mark end of any above + std::ostream& end(std::ostream&); + + /// Switch to colorful output via ANSI escape sequences + void terminal_mode(); + + /// Switch to colorless output (default one) + void no_color_mode(); +} + +#endif diff --git a/musique/result.hh b/musique/result.hh new file mode 100644 index 0000000..2a819fb --- /dev/null +++ b/musique/result.hh @@ -0,0 +1,88 @@ +#ifndef MUSIQUE_RESULT_HH +#define MUSIQUE_RESULT_HH + +#include + +#include "errors.hh" + +/// Holds either T or Error +template +struct [[nodiscard("This value may contain critical error, so it should NOT be ignored")]] Result : tl::expected +{ + using Storage = tl::expected; + + constexpr Result() = default; + + template requires (not std::is_void_v) && std::is_constructible_v + constexpr Result(Args&& ...args) + : Storage( T{ std::forward(args)... } ) + { + } + + template requires std::is_constructible_v + constexpr Result(Arg &&arg) + : Storage(std::forward(arg)) + { + } + + inline Result(Error error) + : Storage(tl::unexpected(std::move(error))) + { + } + + template + requires requires (Arg a) { + { Error { .details = std::move(a) } }; + } + inline Result(Arg a) + : Storage(tl::unexpected(Error { .details = std::move(a) })) + { + } + + // Internal function used for definition of Try macro + inline auto value() && + { + if constexpr (not std::is_void_v) { + // NOTE This line in ideal world should be `return Storage::value()` + // but C++ does not infer that this is rvalue context. + // `std::add_rvalue_reference_t::value()` + // also does not work, so this is probably the best way to express this: + return std::move(*static_cast(this)).value(); + } + } + + /// Fill error location if it's empty and we have an error + inline Result with_location(Location location) && + { + if (!Storage::has_value()) { + if (auto& target = Storage::error().location; !target || target == Location{}) { + target = location; + } + } + return *this; + } + + inline tl::expected to_expected() && + { + return *static_cast(this); + } + + template + requires is_template_v> + auto and_then(Map &&map) && + { + return std::move(*static_cast(this)).and_then( + [map = std::forward(map)](T &&value) { + return std::move(map)(std::move(value)).to_expected(); + }); + } + + using Storage::and_then; + + operator std::optional() && + { + return Storage::has_value() ? std::nullopt : std::optional(Storage::error()); + } +}; + +#endif diff --git a/musique/token.hh b/musique/token.hh new file mode 100644 index 0000000..3ea39fa --- /dev/null +++ b/musique/token.hh @@ -0,0 +1,49 @@ +#ifndef MUSIQUE_TOKEN_HH +#define MUSIQUE_TOKEN_HH + +#include "common.hh" +#include "location.hh" + +/// Lexical token representation for Musique language +struct Token +{ + /// Type of Token + enum class Type + { + Symbol, ///< like repeat or choose or chord + Keyword, ///< like true, false, nil + Operator, ///< like "+", "-", "++", "<" + Chord, ///< chord or single note literal, like "c125" + Numeric, ///< numeric literal (floating point or integer) + Parameter_Separator, ///< "|" separaters arguments from block body + Expression_Separator, ///< ";" separates expressions. Used mainly to separate calls, like `foo 1 2; bar 3 4` + Open_Block, ///< "[" delimits anonymous block of code (potentially a function) + Close_Block, ///< "]" delimits anonymous block of code (potentially a function) + Open_Paren, ///< "(" used in arithmetic or as function invocation sarrounding + Close_Paren ///< ")" used in arithmetic or as function invocation sarrounding + }; + + /// Type of token + Type type; + + /// Matched source code to the token type + std::string_view source; + + /// Location of encountered token + Location location; +}; + +static constexpr usize Keywords_Count = 5; +static constexpr usize Operators_Count = 17; + +std::string_view type_name(Token::Type type); + +/// Token debug printing +std::ostream& operator<<(std::ostream& os, Token const& tok); + +/// Token type debug printing +std::ostream& operator<<(std::ostream& os, Token::Type type); + +template<> struct std::hash { std::size_t operator()(Token const&) const; }; + +#endif diff --git a/musique/try.hh b/musique/try.hh new file mode 100644 index 0000000..2ee019e --- /dev/null +++ b/musique/try.hh @@ -0,0 +1,87 @@ +#ifndef MUSIQUE_TRY_HH +#define MUSIQUE_TRY_HH + +#include "result.hh" + +/// Shorthand for forwarding error values with Result type family. +/// +/// This implementation requires C++ language extension: statement expressions +/// It's supported by GCC and Clang, other compilers i don't know. +/// Inspired by SerenityOS TRY macro +#define Try(Value) \ + ({ \ + auto try_value = (Value); \ + using Trait [[maybe_unused]] = Try_Traits>; \ + if (not Trait::is_ok(try_value)) [[unlikely]] \ + return Trait::yield_error(std::move(try_value)); \ + Trait::yield_value(std::move(try_value)); \ + }) + +/// Abstraction over any value that are either value or error +/// +/// Inspired by P2561R0 +template +struct Try_Traits +{ + template + static constexpr bool is_ok(T const& v) { return Try_Traits::is_ok(v); } + + template + static constexpr auto yield_value(T&& v) { return Try_Traits::yield_value(std::forward(v)); } + + template + static constexpr auto yield_error(T&& v) { return Try_Traits::yield_error(std::forward(v)); } +}; + +template<> +struct Try_Traits> +{ + using Value_Type = std::nullopt_t; + using Error_Type = Error; + + static constexpr bool is_ok(std::optional const& o) + { + return not o.has_value(); + } + + static std::nullopt_t yield_value(std::optional&& err) + { + assert(not err.has_value(), "Trying to yield value from optional that contains error"); + return std::nullopt; + } + + static Error yield_error(std::optional&& err) + { + assert(err.has_value(), "Trying to yield value from optional that NOT constains error"); + return std::move(*err); + } +}; + +template +struct Try_Traits> +{ + using Value_Type = T; + using Error_Type = Error; + + static constexpr bool is_ok(Result const& o) + { + return o.has_value(); + } + + static auto yield_value(Result val) + { + assert(val.has_value(), "Trying to yield value from expected that contains error"); + if constexpr (std::is_void_v) { + } else { + return std::move(*val); + } + } + + static Error yield_error(Result&& val) + { + assert(not val.has_value(), "Trying to yield error from expected with value"); + return std::move(val.error()); + } +}; + +#endif diff --git a/musique/typecheck.hh b/musique/typecheck.hh new file mode 100644 index 0000000..1fab62f --- /dev/null +++ b/musique/typecheck.hh @@ -0,0 +1,47 @@ +#ifndef MUSIQUE_TYPECHECK_HH +#define MUSIQUE_TYPECHECK_HH + +#include + +/// Intrinsic implementation primitive providing a short way to check if arguments match required type signature +static inline bool typecheck(std::vector const& args, auto const& ...expected_types) +{ + return (args.size() == sizeof...(expected_types)) && + [&args, expected_types...](std::index_sequence) { + return ((expected_types == args[I].type) && ...); + } (std::make_index_sequence{}); +} + +/// Intrinsic implementation primitive providing a short way to move values based on matched type signature +static inline bool typecheck_front(std::vector const& args, auto const& ...expected_types) +{ + return (args.size() >= sizeof...(expected_types)) && + [&args, expected_types...](std::index_sequence) { + return ((expected_types == args[I].type) && ...); + } (std::make_index_sequence{}); +} + +/// Intrinsic implementation primitive providing a short way to move values based on matched type signature +template +static inline auto move_from(std::vector& args) +{ + return [&args](std::index_sequence) { + return std::tuple { (std::move(args[I]).*(Member_For_Value_Type::value)) ... }; + } (std::make_index_sequence{}); +} + +/// Shape abstraction to define what types are required once +template +struct Shape +{ + static inline auto move_from(std::vector& args) { return ::move_from(args); } + static inline auto typecheck(std::vector& args) { return ::typecheck(args, Types...); } + static inline auto typecheck_front(std::vector& args) { return ::typecheck_front(args, Types...); } + + static inline auto typecheck_and_move(std::vector& args) + { + return typecheck(args) ? std::optional { move_from(args) } : std::nullopt; + } +}; + +#endif diff --git a/src/unicode.cc b/musique/unicode.cc similarity index 98% rename from src/unicode.cc rename to musique/unicode.cc index a444e5c..34b3128 100644 --- a/src/unicode.cc +++ b/musique/unicode.cc @@ -1,4 +1,5 @@ -#include "musique.hh" +#include +#include static constexpr std::array payloads { 0b0111'1111, 0b0001'1111, 0b0000'1111, 0b0000'0111 diff --git a/musique/unicode.hh b/musique/unicode.hh new file mode 100644 index 0000000..3259d0a --- /dev/null +++ b/musique/unicode.hh @@ -0,0 +1,50 @@ +#ifndef MUSIQUE_UNICODE_HH +#define MUSIQUE_UNICODE_HH + +#include +#include + +/// All unicode related operations +namespace unicode +{ + inline namespace special_runes + { + [[maybe_unused]] constexpr u32 Rune_Error = 0xfffd; + [[maybe_unused]] constexpr u32 Rune_Self = 0x80; + [[maybe_unused]] constexpr u32 Max_Bytes = 4; + } + + /// is_digit returns true if `digit` is ASCII digit + bool is_digit(u32 digit); + + /// is_space return true if `space` is ASCII blank character + bool is_space(u32 space); + + /// is_letter returns true if `letter` is considered a letter by Unicode + bool is_letter(u32 letter); + + /// is_identifier returns true if `letter` is valid character for identifier. + /// + /// It's modifier by is_first_character flag to determine some character classes + /// allowance like numbers, which are only allowed NOT at the front of the identifier + enum class First_Character : bool { Yes = true, No = false }; + bool is_identifier(u32 letter, First_Character is_first_character); +} + +/// utf8 encoding and decoding +namespace utf8 +{ + using namespace unicode::special_runes; + + /// Decodes rune and returns remaining string + auto decode(std::string_view s) -> std::pair; + + /// Returns length of the first rune in the provided string + auto length(std::string_view s) -> usize; + + struct Print { u32 rune; }; +} + +std::ostream& operator<<(std::ostream& os, utf8::Print const& print); + +#endif diff --git a/src/unicode_tables.cc b/musique/unicode_tables.cc similarity index 99% rename from src/unicode_tables.cc rename to musique/unicode_tables.cc index 6828f20..912922f 100644 --- a/src/unicode_tables.cc +++ b/musique/unicode_tables.cc @@ -1,4 +1,6 @@ -#include +#include +#include +#include #include constexpr u32 Max_Latin1 = 0xff; diff --git a/src/value.cc b/musique/value.cc similarity index 99% rename from src/value.cc rename to musique/value.cc index 57477bc..da2a52f 100644 --- a/src/value.cc +++ b/musique/value.cc @@ -1,5 +1,8 @@ -#include -#include +#include +#include +#include +#include +#include #include #include diff --git a/musique/value.hh b/musique/value.hh new file mode 100644 index 0000000..117cd7a --- /dev/null +++ b/musique/value.hh @@ -0,0 +1,150 @@ +#ifndef MUSIQUE_VALUE_HH +#define MUSIQUE_VALUE_HH + +#include "array.hh" +#include "block.hh" +#include "chord.hh" +#include "common.hh" +#include "note.hh" +#include "result.hh" +#include "token.hh" + +/// Representation of any value in language +struct Value +{ + /// Creates value from literal contained in Token + static Result from(Token t); + + /// Create value holding provided boolean + /// + /// Using Explicit_Bool to prevent from implicit casts + static Value from(Explicit_Bool b); + + static Value from(Array &&array); ///< Create value of type array holding provided array + static Value from(Block &&l); ///< Create value of type block holding provided block + static Value from(Chord chord); ///< Create value of type music holding provided chord + static Value from(Note n); ///< Create value of type music holding provided note + static Value from(Number n); ///< Create value of type number holding provided number + static Value from(char const* s); ///< Create value of type symbol holding provided symbol + static Value from(std::string s); ///< Create value of type symbol holding provided symbol + static Value from(std::string_view s); ///< Create value of type symbol holding provided symbol + static Value from(std::vector &&array); ///< Create value of type array holding provided array + + enum class Type + { + Nil, ///< Unit type, used for denoting emptiness and result of some side effect only functions + Bool, ///< Boolean type, used for logic computations + Number, ///< Number type, representing only rational numbers + Symbol, ///< Symbol type, used to represent identifiers + Intrinsic, ///< Intrinsic functions that are implemented in C++ + Block, ///< Block type, containing block value (lazy array/closure/lambda like) + Array, ///< Array type, eager array + Music, ///< Music type, + }; + + Value() = default; + Value(Value const&) = default; + Value(Value &&) = default; + Value& operator=(Value const&) = default; + Value& operator=(Value &&) = default; + + /// Contructs Intrinsic, used to simplify definition of intrinsics + inline Value(Intrinsic intr) : type{Type::Intrinsic}, intr(intr) + { + } + + Type type = Type::Nil; + bool b; + Number n; + Intrinsic intr; + Block blk; + Chord chord; + Array array; + + // TODO Most strings should not be allocated by Value, but reference to string allocated previously + // Wrapper for std::string is needed that will allocate only when needed, middle ground between: + // std::string - always owning string type + // std::string_view - not-owning string type + std::string s{}; + + /// Returns truth judgment for current type, used primarly for if function + bool truthy() const; + + /// Returns false judgment for current type, used primarly for if function + bool falsy() const; + + /// Calls contained value if it can be called + Result operator()(Interpreter &i, std::vector args); + + /// Index contained value if it can be called + Result index(Interpreter &i, unsigned position) const; + + /// Return elements count of contained value if it can be measured + usize size() const; + + bool operator==(Value const& other) const; + + std::partial_ordering operator<=>(Value const& other) const; +}; + +template +struct Member_For_Value_Type {}; + +template<> struct Member_For_Value_Type +{ static constexpr auto value = &Value::b; }; + +template<> struct Member_For_Value_Type +{ static constexpr auto value = &Value::n; }; + +template<> struct Member_For_Value_Type +{ static constexpr auto value = &Value::s; }; + +template<> struct Member_For_Value_Type +{ static constexpr auto value = &Value::intr; }; + +template<> struct Member_For_Value_Type +{ static constexpr auto value = &Value::blk; }; + +template<> struct Member_For_Value_Type +{ static constexpr auto value = &Value::array; }; + +template<> struct Member_For_Value_Type +{ static constexpr auto value = &Value::chord; }; + +/// Returns type name of Value type +std::string_view type_name(Value::Type t); + +std::ostream& operator<<(std::ostream& os, Value const& v); +template<> struct std::hash { std::size_t operator()(Value const&) const; }; + +/// Returns if type can be indexed +static constexpr bool is_indexable(Value::Type type) +{ + return type == Value::Type::Array || type == Value::Type::Block; +} + +/// Returns if type can be called +static constexpr bool is_callable(Value::Type type) +{ + return type == Value::Type::Block || type == Value::Type::Intrinsic; +} + + +/// Binary operation may be vectorized when there are two argument which one is indexable and other is not +static inline bool may_be_vectorized(std::vector const& args) +{ + return args.size() == 2 && (is_indexable(args[0].type) != is_indexable(args[1].type)); +} + +template +Result wrap_value(Result &&value) +{ + return std::move(value).map([](auto &&value) { return Value::from(std::move(value)); }); +} + +Value wrap_value(auto &&value) +{ + return Value::from(std::move(value)); +} + +#endif diff --git a/musique/value_algorithms.hh b/musique/value_algorithms.hh new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/musique/value_algorithms.hh @@ -0,0 +1 @@ + diff --git a/scripts/debug.mk b/scripts/debug.mk index d6b5dd5..6e7e396 100644 --- a/scripts/debug.mk +++ b/scripts/debug.mk @@ -6,6 +6,6 @@ bin/debug/musique: $(Debug_Obj) bin/debug/main.o bin/bestline.o include/*.hh @echo "CXX $@" @$(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) $(CPPFLAGS) -o $@ $(Debug_Obj) bin/bestline.o bin/debug/main.o $(LDFLAGS) $(LDLIBS) -bin/debug/%.o: src/%.cc include/*.hh +bin/debug/%.o: musique/%.cc include/*.hh @echo "CXX $@" @$(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) $(CPPFLAGS) -o $@ $< -c diff --git a/scripts/release.mk b/scripts/release.mk index b6e66c1..ef143c5 100644 --- a/scripts/release.mk +++ b/scripts/release.mk @@ -1,9 +1,9 @@ Release_Obj=$(addprefix bin/,$(Obj)) -bin/%.o: src/%.cc include/*.hh +bin/%.o: musique/%.cc @echo "CXX $@" @$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $< -c -bin/musique: $(Release_Obj) bin/main.o bin/bestline.o include/*.hh lib/midi/libmidi-alsa.a +bin/musique: $(Release_Obj) bin/main.o bin/bestline.o lib/midi/libmidi-alsa.a @echo "CXX $@" @$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $(Release_Obj) bin/bestline.o bin/main.o $(LDFLAGS) $(LDLIBS) diff --git a/scripts/test.mk b/scripts/test.mk index 8e98711..b8e2c33 100644 --- a/scripts/test.mk +++ b/scripts/test.mk @@ -1,16 +1,3 @@ -Tests= \ - context.o \ - environment.o \ - interpreter.o \ - lex.o \ - main.o \ - number.o \ - parser.o \ - unicode.o \ - value.o - -Test_Obj=$(addprefix bin/debug/tests/,$(Tests)) - test: bin/debug/musique scripts/test.py test examples