diff --git a/CHANGELOG.md b/CHANGELOG.md index c89cd75..155fbc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Builtin function documentation generation from C++ Musique implementation source code +- New builtins: digits +- Negative numbers! + +### Removed + +- Release builder, since it's separate part of the project + +### Fixed + +- `ceil`, `round`, `floor` didn't behave well with negative numbers +- `duration` wasn't filling note length from context and summed all notes inside chord, when it should take max +- `try` evaluated arguments too quickly + ## [0.3.1] ### Fixed diff --git a/Makefile b/Makefile index 370ddf6..01f7286 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,9 @@ doc/musique-vs-languages-cheatsheet.html: doc/musique-vs-languages-cheatsheet.te doc/wprowadzenie.html: doc/wprowadzenie.md pandoc -o $@ $< -s --toc +doc/functions.html: musique/interpreter/builtin_functions.cc scripts/document-builtin.py + scripts/document-builtin.py -o $@ $< + .PHONY: clean doc doc-open all test unit-tests release install $(shell mkdir -p $(subst musique/,bin/$(os)/,$(shell find musique/* -type d))) diff --git a/doc/functions.md b/doc/functions.md deleted file mode 100644 index 7d32369..0000000 --- a/doc/functions.md +++ /dev/null @@ -1,115 +0,0 @@ -# Lista wbudowanych funkcji języka Musique - -* `bmp value` – zmienia wartość BMP z domyślnej na `value`; - -`value` musi być liczbą całkowitą, domyślnie `120`; - -* `call function args` – funkcja wywołująca funkcję `function`, która przekazuje `function` `args` jako argumenty wywoływanej fukncji; - -* `ceil value` – operacja podobna do matematycznej funkcji podłogi (zaokrąglenie liczby do pierwszej liczby całkowitej mniejszej lub równej tej liczbie); - - `value` – musi być to wartość o typie Number lub tablica takich wartości; - -* `chord (notes)` – konstruuje akord z `notes`: - - `notes` – `notes` definiowane są następująco: `( )`: - - np. `(c 4 1)` – dźwięk C w 4 oktawie, o długości całej nuty; - -* `down value` – sekwencyjnie zwraca liczby całkowite, począwszy od `value` do 0: - - `value` – musi być liczbą całkowitą; - -* `flat args` – łączy `args` w tablicę bez zagnieżdżeń (tzn. "odpakowuje" zawartość zagnieżdżonych tablic i zawiera je w pojedyńczej tabeli): - - `args` - tablica, w tym tablica z zagnieżdżeniami; - -* `floor value` – operacja podobna do matematycznej funkcji podłogi (zaokrąglenie liczby do pierwszej liczby całkowitej większej lub równej tej liczbie); - - `value` – musi być to zmienna o typie Number lub tablica takich zmiennych; - -* `fold args` – używa elementów tablicy jako argumentów podanej funkcji: - - `args` – postaci `tablica funkcja` lub `tablica wartość_startowa funkcja`; - -* `for vect` – iteruje po elementach wektora `vect`: - - `vect` - kontener wartości, musi posiadać typ Vector; - -* `hash vect` – standardowa funkcja haszująca, zwraca jeden hash połączonych wartości z `vect`: - - `vect` - kontener wartości, mogą być dowolnego typu; - -* `if cond [if_true] [if_false]` – wyrażenie warunkowe: jeżeli `cond` będzie prawdą, zostanie wykonany kod z `[if_true]`, w przeciwnym wypadku wykonany zostanie kod z `[if_false]` – fragment `[if_false]` jest opcjonalny; - -* `incoming args` – pozwala na rozpatrzenie przychodzących komunikatów MIDI (`note_on` i `note_off`), odpowiednio; - - `args` – konstrukcja `komunikat, nuta`; - -* `instrument args` – pozwala na zmianę instrumentu: - - `args` – może przyjmować sam numer programu, lub parę `numer_programu, kanał`; - -* `len args` – zwraca długość kontenera `args`, a jeżeli `args` nie jest wektorem ustawia domyślną długość trwania dźwięku, domyślnie ćwierćnuta; - -* `max args` – zwraca maksimum z `args`; - -* `min args` – zwraca minimum z `args`; - -* `mix args` – algorytmicznie miesza wszystkie elementy z `args`: - - `args` – tablica elementów, może być tablicą z zagnieżdżeniami; - -* `note_off args` – w zależności od kształtu `args`: - - jeżeli `args` są w postaci `(kanał, nuta)` – wyłącza nutę na danym kanale: - - `kanał` – liczba całkowita; - - `nuta` – postać podobna do `notes` z `chord notes`; - - jeżeli `args` są w postaci `(kanał, akord)` – wyłącza wszystkie nuty z danego akordu na danym kanale; - -* `note_on args` – analogicznie do `note_off args`; - -* `nprimes value` – generuje `value` kolejnych liczb pierwszych: - - `value` – musi być typu Number; - -* `oct value` – analogicznie do `bpm value`, wartość domyślna to 4; - -* `par args` – gra współbieżnie pierwszy dźwięk z `args` z pozostałymi dźwiękami z `args`: - - `args` – postać `(note, ...)`, powinien być rozmiaru co najmniej 2: - - `note` – postać podobna do `notes` z `chords notes`; - -* `partition args` – dzieli `args` na dwie grupy wedle danej funkcji: - - `args` – powinno przyjąć formę `(funkcja, tablica())` - -* `permute args` – permutuje `args`: - - `args` – tablica obiektów; - -* `pgmchange args` – analogicznie do `instrument args`; - -* `play args` – gra `args`: - - `args` – mogą być to pojedyncze nuty, tablica nut oraz bloki kodu (nuty analogicznie jak w `chord notes`); - -* `program_change args` – analogicznie do `instrument args`; - -* `range args` – zwraca tablicę wartości liczbowych w podanych w `args` zakresie: - - `args` – postać `stop`, `start stop` lub `start stop step`; - -* `reverse args` – odwraca kolejność elementów `args`; - - `args` – powinna być to tablica; - -* `rotate args` – przenosi na koniec tablicy wskazaną ilość elementów: - - `args` – musi być postaci `liczba tablica`; - -* `round value` – zaokrągla wartość zgodnie z reguałmi matematyki: - - `value` – musi być to wartość liczbowa; - -* `shuffle args` – tasuje elementy `args`: - - `args` – powinna być to tablica; - -* `sim` – #TODO - -* `sort args` – sortuje elementy `args`: - - `args` – powinna być to tablica; - -* `try args` – próbuje wykonać wszystkie bloki kodu poza ostatnim, a jeżeli w trakcie tej próby natrafi na błąd, wykonuje ostatni blok: - - `args` – musi być to co najmniej jeden blok kodu Musique; - -* `typeof variable` – zwraca typ wskazanej `variable`; - -* `uniq args` – zwraca tablicę elementów, z której usunięto następujące po sobie powtórzenia: - - `args` – powinna być to tablica; - -* `unique args` – zwraca tablicę elementów, z której usunięto powtórzenia: - - `args` – powinna być to tablica; - -* `up value` – analogicznie do `down value`; - -* `update args` – aktualizuje element tablicy do nowej wartości: - - `args` – postaci `tablica indeks wartość`; - diff --git a/doc/musique-vs-languages-cheatsheet.template b/doc/musique-vs-languages-cheatsheet.template index a8aa2ac..b423933 100644 --- a/doc/musique-vs-languages-cheatsheet.template +++ b/doc/musique-vs-languages-cheatsheet.template @@ -74,7 +74,7 @@ r add 1, 3 n Wywołanie funkcji n nie przymujących argumentów -m constant := (say 42, 10); +m constant := (say 42, 10), m say (call foo) p def constant(): p print(42) @@ -199,7 +199,7 @@ c w SonicPi nie ma domyślnej długości nuty; c za każdym razem trzeba ją definiować n Zmiana oktawy do 4tej -m oct 4; +m oct 4, c w SonicPi domyślna oktawa c jest ustalona jako 4 diff --git a/musique/errors.cc b/musique/errors.cc index 39a7e10..4bdb3e2 100644 --- a/musique/errors.cc +++ b/musique/errors.cc @@ -201,7 +201,12 @@ std::ostream& operator<<(std::ostream& os, Error const& err) print_error_line(loc); os << "Variables can only be references in scope (block) where they been created\n"; - os << "or from parent blocks to variable block\n"; + os << "or from parent blocks to variable block\n\n"; + + pretty::begin_comment(os); + os << "Maybe you want to defined it. To do this you must use ':=' operator.\n"; + os << " name := value\n"; + pretty::end(os); }, [&](errors::Unrecognized_Character const& err) { os << "I encountered character in the source code that was not supposed to be here.\n"; @@ -268,7 +273,7 @@ std::ostream& operator<<(std::ostream& os, Error const& err) [&](errors::Unsupported_Types_For const& err) { switch (err.type) { - case errors::Unsupported_Types_For::Function: + break; case errors::Unsupported_Types_For::Function: { os << "I tried to call function '" << err.name << "' but you gave me wrong types for it!\n"; @@ -281,9 +286,26 @@ std::ostream& operator<<(std::ostream& os, Error const& err) os << " " << possibility << '\n'; } } - break; - case errors::Unsupported_Types_For::Operator: + break; case errors::Unsupported_Types_For::Operator: { + if (err.name == "=") { + os << "Operator '=' expects name on it's left side.\n"; + os << "\n"; + + print_error_line(loc); + + pretty::begin_comment(os); + os << "If you want to test if two values are equal use '==' operator:\n"; + // TODO Maybe we can use code serialization mechanism to print here actual equation + // but transformed to account for use of == operator. If produced string is too big + // then we can skip and show this silly example + os << " 3 == 4\n"; + os << "If you want to change element of an array use update function:\n"; + os << " instead of a[i] = x you may write a = update a i x\n"; + pretty::end(os); + return; + } + os << "I tried and failed to evaluate operator '" << err.name << "' due to values with wrong types provided\n"; os << "Make sure that both values matches one of supported signatures listed below!\n"; os << '\n'; @@ -297,7 +319,6 @@ std::ostream& operator<<(std::ostream& os, Error const& err) os << " " << possibility << '\n'; } } - break; } }, diff --git a/musique/errors.hh b/musique/errors.hh index 90c9dfe..b5e546a 100644 --- a/musique/errors.hh +++ b/musique/errors.hh @@ -45,19 +45,19 @@ namespace errors /// When user forgot semicolon or brackets struct Expected_Expression_Separator_Before { - std::string_view what; + std::string what; }; /// When some keywords are not allowed in given context struct Unexpected_Keyword { - std::string_view keyword; + std::string keyword; }; /// When user tried to use operator that was not defined struct Undefined_Operator { - std::string_view op; + std::string op; }; /// When user tries to use operator with wrong arity of arguments @@ -67,7 +67,7 @@ namespace errors enum Type { Operator, Function } type; /// Name of operation - std::string_view name; + std::string name; /// Arity that was expected by given operation size_t expected_arity; @@ -79,15 +79,15 @@ namespace errors /// When user tried to call something that can't be called struct Not_Callable { - std::string_view type; + std::string 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; + std::string type_name; + std::string source; + std::string context; }; /// When user provides wrong type for given operation @@ -108,6 +108,10 @@ namespace errors { /// Name of variable std::string name; + + // TODO std::vector similar_names; + // Pull all names reachable from given point and similar onces + // put in this array }; /// When user tries to invoke some MIDI action but haven't established MIDI connection @@ -155,13 +159,13 @@ namespace errors struct Unexpected_Token { /// Type of the token - std::string_view type; + std::string type; /// Source of the token - std::string_view source; + std::string source; /// Where this token was encountered that was unexpected? - std::string_view when; + std::string when; }; } diff --git a/musique/interpreter/builtin_functions.cc b/musique/interpreter/builtin_functions.cc index d92d5d0..026a9b0 100644 --- a/musique/interpreter/builtin_functions.cc +++ b/musique/interpreter/builtin_functions.cc @@ -11,6 +11,14 @@ #include #include +/// This macro implements functions that are only implemented as forwarding +/// all arguments to another function +#define Forward_Implementation(New_Function_Name, Implementation) \ + static inline Result New_Function_Name(Interpreter &interpreter, std::vector args) \ + { \ + return Implementation(interpreter, std::move(args)); \ + } + /// Check if type has index method template concept With_Index_Method = requires (T &t, Interpreter interpreter, usize position) { @@ -103,6 +111,39 @@ static Result ctx_read_write_property(Interpreter &interpreter, std::vect return Value{}; } +//: Funkcja `bpm` pozwala na zapisywanie i odczytywanie wartości BPM z aktualnego kontekstu. +//: +//: Domyślną wartością jest 120. +//: # Odczytywanie wartości z kontekstu +//: ``` +//: > call bpm +//: 120 +//: ``` +//: # Zapisywanie wartości BPM do aktualnego kontekstu +//: ``` +//: > bpm 144 +//: 144 +//: ``` +Forward_Implementation(builtin_bpm, ctx_read_write_property<&Context::bpm>) + +//: Funkcja `oct` pozwala na zapisywanie i odczytywanie wartości oktawy z aktualnego kontekstu. +//: +//: Wartość ta jest używana w momencie odtwarzania dźwięków nie posiadających ustalonego numeru oktawy: +//: `c` zostanie uzupełnione oktawą domyślną z kontekstu, `c5` zachowa swój nr oktawy. +//: +//: Domyślną wartością jest 120. +//: # Odczytywanie wartości z kontekstu +//: ``` +//: > call bpm +//: 120 +//: ``` +//: # Zapisywanie wartości BPM do aktualnego kontekstu +//: ``` +//: > bpm 144 +//: 144 +//: ``` +Forward_Implementation(builtin_oct, ctx_read_write_property<&Context::octave>) + /// Iterate over array and it's subarrays to create one flat array static Result into_flat_array(Interpreter &interpreter, std::span args) { @@ -171,13 +212,41 @@ invalid_argument_type: }, }; } +//: Funkcja `ceil` zwraca liczbę zaokrągloną do pierwszej większej liczby całkowitej. +//: +//: # Przykład +//: ``` +//: > ceil (4.3) +//: 4 +//: ``` +Forward_Implementation(builtin_ceil, apply_numeric_transform<&Number::ceil>) + +//: Funkcja `floor` zwraca liczbę zaokrągloną do pierwszej mniejszej liczby całkowitej. +//: +//: # Przykład +//: ``` +//: > floor (4.3) +//: 5 +//: ``` +Forward_Implementation(builtin_floor, apply_numeric_transform<&Number::floor>) + +//: Funkcja `round` zwraca liczbę zaokrągloną do najbliższej liczby parzystej. +//: +//: # Przykład +//: ``` +//: > round (4.5) +//: 4 +//: > round (3.5) +//: 4 +//: ``` +Forward_Implementation(builtin_round, apply_numeric_transform<&Number::round>) /// Direction used in range definition (up -> 1, 2, 3; down -> 3, 2, 1) enum class Range_Direction { Up, Down }; /// Create range according to direction and specification, similar to python template -static Result builtin_range(Interpreter&, std::vector args) +static Result range(Interpreter&, std::vector args) { auto start = Number(0), stop = Number(0), step = Number(1); @@ -209,8 +278,47 @@ static Result builtin_range(Interpreter&, std::vector args) } return array; } +//: Funkcja `range` zwraca listę wartości liczbowych w podanych w zakresach `start, stop, step`. +//: +//: # Przykład +//: ``` +//: > range 1 10 2 +//: (1, 3, 5, 7, 9) +//: > range 13 17 +//: (13, 14, 15, 16) +//: ``` +Forward_Implementation(builtin_range, range) -/// Send MIDI Program Change message +//: Funkcja `up` zwraca listę wartości liczbowych od 0 do zadanej wartości pomniejszonej o 1. +//: +//: # Przykład +//: ``` +//: > up 10 +//: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) +//: ``` +Forward_Implementation(builtin_up, range) + +//: Funkcja `down` zwraca listę wartości liczbowych od zadanej wartości pomniejszonej o 1 do 0. +//: +//: # Przykład +//: ``` +//: > down 10 +//: (9, 8, 7, 6, 5, 4, 3, 2, 1, 0) +//: > down 5 10 +//: (9, 8, 7, 6, 5) +//: ``` +Forward_Implementation(builtin_down, range) + +//: Funkcja `instrument` pozwala na wybór instrumentu na danym kanale MIDI. +//: # Ustawienie instrumentu 4 +//: ``` +//: instrument 4 +//: ``` +//: # Ustawienie instrumentu 4 na kanale 6 +//: ``` +//: instrument 6 4 +//: ``` +//: Przyporządkowanie numerów instrumentów do standardowych nazw znajdziesz [tutaj](http://midi.teragonaudio.com/tutr/gm.htm#Patch) static auto builtin_program_change(Interpreter &i, std::vector args) -> Result { if (auto a = match(args)) { auto [program] = *a; @@ -264,6 +372,12 @@ static std::optional action_play(Interpreter &i, Value v) return {}; } +//: Funkcja `play` odgrywa zadaną nutę lub sekwencję. +//: +//: # Przykład +//: ``` +//: > play chord c e g +//: ``` /// Play notes template> static inline Result builtin_play(Interpreter &interpreter, Container args) @@ -289,6 +403,12 @@ static inline Result builtin_play(Interpreter &interpreter, Container arg return {}; } +//: Funkcja `par` odgrywa pierwszy element przez sumę długości pozostałych elementów. Odegranie pierwszego elementu wraz z pozostałymi następuje współbieżnie. +//: +//: # Przykład +//: ``` +//: > par c (1/2) b b e +//: ``` /// Play first argument while playing all others static Result builtin_par(Interpreter &interpreter, std::vector args) { Try(ensure_midi_connection_available(interpreter, "par")); @@ -324,6 +444,14 @@ static Result builtin_par(Interpreter &interpreter, std::vector ar return result; } +//: Funkcja `sim` odgrywa zadane sekwencje symultanicznie. +//: +//: # Przykład +//: ``` +//: > A := c e g d +//: > B := f f a a +//: > sim A B +//: ``` /// Plays each argument simultaneously static Result builtin_sim(Interpreter &interpreter, std::vector args) { @@ -452,6 +580,13 @@ static inline size_t upper_sieve_bound_to_yield_n_primes(size_t n_primes) return std::ceil(x); } +//: Funkcja `nprimes` zwraca zadaną liczbę kolejnych liczb pierwszych. +//: +//: # Przykład +//: ``` +//: > nprimes 4 +//: (2, 3, 5, 7) +//: ``` /// Generate n primes static Result builtin_primes(Interpreter&, std::vector args) { @@ -501,6 +636,16 @@ static Result builtin_primes(Interpreter&, std::vector args) }; } +//: Funkcja `nprimes` zwraca zadaną liczbę kolejnych liczb pierwszych. +//: +//: # Przykład +//: ``` +//: > for (nprimes 4) (say) +//: 2 +//: 3 +//: 5 +//: 7 +//: ``` /// Iterate over container static Result builtin_for(Interpreter &i, std::vector args) { @@ -521,6 +666,12 @@ static Result builtin_for(Interpreter &i, std::vector args) } } +//: Funkcja `fold` TODO. +//: +//: # Przykład +//: ``` +//: TODO +//: ``` /// Fold container static Result builtin_fold(Interpreter &interpreter, std::vector args) { constexpr auto guard = Guard<2> { @@ -547,6 +698,14 @@ static Result builtin_fold(Interpreter &interpreter, std::vector a return guard.yield_error(); } +//: Funkcja `map` aplikuje zadaną funkcję do każdego argumentu. +//: +//: # Przykład +//: ``` +//: > map up (nprimes 3) +//: ((0, 1), (0, 1, 2), (0, 1, 2, 3, 4)) +//: ``` + static Result builtin_map(Interpreter &interpreter, std::vector args) { static constexpr auto guard = Guard<2> { @@ -578,6 +737,12 @@ static Result builtin_map(Interpreter &interpreter, std::vector ar return result; } +//: Funkcja `scan` oblicza sumę prefiksową (dodaje do siebie wszystkie liczby od 1 do danej liczby). +//: +//: # Przykład +//: ``` +//: +//: ``` /// Scan computes inclusive prefix sum static Result builtin_scan(Interpreter &interpreter, std::vector args) { @@ -600,6 +765,19 @@ static Result builtin_scan(Interpreter &interpreter, std::vector a }; } +//: Funkcja `if` wykonuje określony blok po spełnieniu warunku, alternatywnie wykonuje inny blok kodu w przeciwnym wypadku. +//: +//: # Przykład +//: ``` +//: > if true (say 42) +//: 42 +//: > if false (say 42) +//: > +//: > if true (say 42) (say 0) +//: 42 +//: > if false (say 42) (say 0) +//: 0 +//: ``` /// Execute blocks depending on condition static Result builtin_if(Interpreter &i, std::span args) { static constexpr auto guard = Guard<2> { @@ -631,6 +809,18 @@ static Result builtin_if(Interpreter &i, std::span args) { return Value{}; } +//: Funkcja `while` wykonuje określony blok dopóki warunek jest spełniony. +//: +//: # Przykład +//: ``` +//: > i := 0 +//: > while (i < 10) (say i, i += 2) +//: 0 +//: 2 +//: 4 +//: 6 +//: 8 +//: ``` /// Loop block depending on condition static Result builtin_while(Interpreter &i, std::span args) { static constexpr auto guard = Guard<2> { @@ -654,36 +844,61 @@ static Result builtin_while(Interpreter &i, std::span args) { return Value{}; } +//: Funkcja `try` przystępuje do wykonania bloków kodu, a jeżeli którykolwiek z nich zakończy się niepowodzeniem, wykonuje ostatni. Jeżeli ostatni też zakończy się niepowodzeniem, to trudno. +//: +//: # Przykład +//: ``` +//: > try (nprimes -1) (say 0) +//: 0 +//: > try (nprimes 2) (say 0) +//: (2, 3) +//: ``` +//: # W przypadku przekazania jednego bloku jako argumentu `try` zwróci pustą wartość w przypadku napotkania błędu podczas wykonania tego bloku. +//: ``` +//: > try (nprimes -1) +//: > +//: ``` /// Try executing all but last block and if it fails execute last one -static Result builtin_try(Interpreter &interpreter, std::vector args) +static Result builtin_try(Interpreter &interpreter, std::span args) { - constexpr auto guard = Guard<1> { - .name = "try", - .possibilities = { - "(...function) -> any" - } - }; - if (args.size() == 1) { - auto callable = Try(guard.match(args[0])); - return std::move(*callable)(interpreter, {}).value_or(Value{}); + // TODO This should be abstracted + auto result = (args[0].type == Ast::Type::Block) + ? interpreter.eval((Ast)args[0].arguments.front()) + : interpreter.eval((Ast)args[0]); + return result.value_or(Value{}); } Value success; - for (usize i = 0; i+1 < args.size(); ++i) { - auto callable = Try(guard.match(args[i])); - if (auto result = std::move(*callable)(interpreter, {})) { - success = *std::move(result); - } else { - auto callable = Try(guard.match(args.back())); - return std::move(*callable)(interpreter, {}); + for (auto const& node : args.subspan(0, args.size()-1)) { + auto result = (node.type == Ast::Type::Block) + ? interpreter.eval((Ast)node.arguments.front()) + : interpreter.eval((Ast)node); + + if (result.has_value()) { + success = *result; + continue; } + + return (args.back().type == Ast::Type::Block) + ? interpreter.eval((Ast)args.back().arguments.front()) + : interpreter.eval((Ast)args.back()); } return success; } +//: Funkcja `update` aktualizuje dany element listy na nową wartość. +//: +//: # Przykład +//: ``` +//: > A := down 5 +//: > A +//: (4, 3, 2, 1, 0) +//: > update A 3 7 +//: (4, 3, 2, 7, 0) +//: ``` /// Update value inside of array static Result builtin_update(Interpreter &i, std::vector args) { @@ -714,6 +929,15 @@ static Result builtin_update(Interpreter &i, std::vector args) return guard.yield_error(); } +//: Funkcja `typeof` zwraca typ zmiennej. +//: # Przykład +//: ``` +//: > A := down 5 +//: > A +//: (4, 3, 2, 1, 0) +//: > typeof A +//: array +//: ``` /// Return typeof variable static Result builtin_typeof(Interpreter&, std::vector args) { @@ -721,6 +945,11 @@ static Result builtin_typeof(Interpreter&, std::vector args) return Symbol(type_name(args.front())); } +//: Funkcja `len` ustawia domyślną długość nuty. +//: # Przykład – ustawienie domyślnej długości nuty na ósemkę +//: ``` +//: len (1/8) +//: ``` /// Return length of container or set/get default length to play static Result builtin_len(Interpreter &i, std::vector args) { @@ -752,6 +981,15 @@ Result traverse(Interpreter &interpreter, Value &&value, auto &&lambda) return value; } +//: Funkcja `set_len` przydziela wszystkim elementom listy zadaną długość. +//: # Przykład +//: ``` +//: > A := c d e f +//: > A +//: (c, d, e, f) +//: > set_len (1/8) A +//: (c 1/8, d 1/8, e 1/8, f 1/8) +//: ``` /// Set length (first argument) to all other arguments preserving their shape static Result builtin_set_len(Interpreter &interpreter, std::vector args) { @@ -781,6 +1019,15 @@ static Result builtin_set_len(Interpreter &interpreter, std::vector A := c d e f +//: > A +//: (c, d, e, f) +//: > set_oct 5 A +//: (c5, d5, e5, f5) +//: ``` /// Set octave (first argument) to all other arguments preserving their shape static Result builtin_set_oct(Interpreter &interpreter, std::vector args) { @@ -810,27 +1057,53 @@ static Result builtin_set_oct(Interpreter &interpreter, std::vector duration a +//: 1/4 +//: > duration chord a b c +//: 3/4 +//: ``` static Result builtin_duration(Interpreter &interpreter, std::vector args) { auto total = Number{}; for (auto &arg : args) { Try(traverse(interpreter, std::move(arg), [&](Chord &c) { + auto chord_length = Number(); for (Note ¬e : c.notes) { - if (note.length) { - total += *note.length; - } + chord_length = std::max(chord_length, note.length ? *note.length : interpreter.current_context->length); } + total += chord_length; })); } return total; } +//: Funkcja `flat` łączy zadane element w listę bez zagnieżdżeń (tzn. "odpakowuje" zawartość zagnieżdżonych list i zawiera je w pojedyńczej listy). +//: +//: # Przykład +//: ``` +//: > B := chord a b c +//: > flat B a b c 1 +//: (chord (a, b, c), a, b, c, 1) +//: ``` /// Join arguments into flat array static Result builtin_flat(Interpreter &i, std::vector args) { return Try(into_flat_array(i, std::move(args))); } +//: Funkcja `pick` zwraca pseudo-losowo element z listy argumentów. +//: +//: # Przykład +//: ``` +//: > pick a b c +//: c +//: > pick a b c +//: b +//: ``` /// Pick random value from arugments static Result builtin_pick(Interpreter &i, std::vector args) { @@ -843,6 +1116,13 @@ static Result builtin_pick(Interpreter &i, std::vector args) return array[dist(rnd)]; } +//: Funkcja `shuffle` pseudo-losowo tasuje elementy z listy argumentów. +//: +//: # Przykład +//: ``` +//: > shuffle a b c +//: (b, a, c) +//: ``` /// Shuffle arguments static Result builtin_shuffle(Interpreter &i, std::vector args) { @@ -852,6 +1132,17 @@ static Result builtin_shuffle(Interpreter &i, std::vector args) return array; } +//: Funkcja `permute` permutuje elementy z listy argumentów. +//: +//: # Przykład +//: ``` +//: > A := permute a b c +//: > A +//: (b, c, a) +//: > B := permute A +//: > B +//: (b, a, c) +//: ``` /// Permute arguments static Result builtin_permute(Interpreter &i, std::vector args) { @@ -860,6 +1151,18 @@ static Result builtin_permute(Interpreter &i, std::vector args) return array; } +//: Funkcja `sortuje` elementy od najmniejszego do największego (w tym nuty). +//: +//: # Sortowanie liczb +//: ``` +//: > sort 64 7 112 99 +//: (7, 64, 99, 112) +//: ``` +//: # Sortowanie nut +//: ``` +//: > sort c# b a g +//: (c#, g, a, b) +//: ``` /// Sort arguments static Result builtin_sort(Interpreter &i, std::vector args) { @@ -868,6 +1171,13 @@ static Result builtin_sort(Interpreter &i, std::vector args) return array; } +//: Funkcja `reverse` odwraca kolejność zadanych elementów. +//: +//: # Przykład +//: ``` +//: > reverse c e g b +//: (b, g, e, c) +//: ``` /// Reverse arguments static Result builtin_reverse(Interpreter &i, std::vector args) { @@ -876,6 +1186,13 @@ static Result builtin_reverse(Interpreter &i, std::vector args) return array; } +//: Funkcja `min` zwraca najmniejszy element z podanych argumentów. +//: +//: # Przykład +//: ``` +//: > min 42 37 8 99 +//: 8 +//: ``` /// Get minimum of arguments static Result builtin_min(Interpreter &i, std::vector args) { @@ -885,6 +1202,13 @@ static Result builtin_min(Interpreter &i, std::vector args) return Value{}; } +//: Funkcja `max` zwraca największy element z podanych argumentów. +//: +//: # Przykład +//: ``` +//: > max 42 37 8 99 +//: 99 +//: ``` /// Get maximum of arguments static Result builtin_max(Interpreter &i, std::vector args) { @@ -894,6 +1218,12 @@ static Result builtin_max(Interpreter &i, std::vector args) return Value{}; } +//: Funkcja `partition` dzieli zadany zbiór na dwa rozłączne względem zadanej funkcji. +//: +//: # Przykład +//: ``` +//: TODO +//: ``` /// Parition arguments into 2 arrays based on predicate static Result builtin_partition(Interpreter &i, std::vector args) { @@ -920,6 +1250,13 @@ static Result builtin_partition(Interpreter &i, std::vector args) }}; } +//: Funkcja `rotate` przenosi na koniec listy zadaną ilość elementów. +//: +//: # Przykład +//: ``` +//: > rotate 2 (1, 2, 3, 4) +//: (3, 4, 1, 2) +//: ``` /// Rotate arguments by n steps static Result builtin_rotate(Interpreter &i, std::vector args) { @@ -931,6 +1268,11 @@ static Result builtin_rotate(Interpreter &i, std::vector args) if (args.size()) { if (auto const offset_source = get_if(args.front())) { + + if (args.size() == 1) { + return Array{}; + } + auto offset = offset_source->as_int(); auto array = Try(flatten(i, std::span(args).subspan(1))); if (offset > 0) { @@ -947,6 +1289,13 @@ static Result builtin_rotate(Interpreter &i, std::vector args) return guard.yield_error(); } +//: Funkcja `unique` zwraca listę argumentów z wyłączeniem powtórzeń. +//: +//: # Przykład +//: ``` +//: > unique 1 2 3 2 2 3 4 +//: (1, 2, 3, 4) +//: ``` /// Returns unique collection of arguments static Result builtin_unique(Interpreter &i, std::vector args) { @@ -963,6 +1312,13 @@ static Result builtin_unique(Interpreter &i, std::vector args) return result; } +//: Funkcja `uniq` usuwa z listy argumentów następujące po sobie powtórzenia elementów. +//: +//: # Przykład +//: ``` +//: > uniq 1 2 3 2 2 2 2 2 2 3 4 +//: (1, 2, 3, 2, 3, 4) +//: ``` /// Returns arguments with all successive copies eliminated static Result builtin_uniq(Interpreter &i, std::vector args) { @@ -980,6 +1336,13 @@ static Result builtin_uniq(Interpreter &i, std::vector args) return result; } +//: Funkcja `hash` zwraca wynik działania funkcji skrótu. +//: +//: # Przykład +//: ``` +//: > hash 4 +//: 177902120014 +//: ``` static Result builtin_hash(Interpreter&, std::vector args) { return Number( @@ -989,6 +1352,13 @@ static Result builtin_hash(Interpreter&, std::vector args) ); } +//: Funkcja `chord` zwraca akord złożony z nut podanych jako argumenty. +//: +//: # Przykład +//: ``` +//: > chord c# e a# +//: chord (c#, e, a#) +//: ``` /// Build chord from arguments static Result builtin_chord(Interpreter &i, std::vector args) { @@ -996,7 +1366,12 @@ static Result builtin_chord(Interpreter &i, std::vector args) Try(create_chord(chord.notes, i, std::move(args))); return chord; } - +//: Funkcja `note_on` włącza nutę (nuty) na danym kanale. +//: +//: # Przykład +//: ``` +//: TODO +//: ``` /// Send MIDI message Note On static Result builtin_note_on(Interpreter &interpreter, std::vector args) { @@ -1012,6 +1387,7 @@ static Result builtin_note_on(Interpreter &interpreter, std::vectorfill(note); interpreter.midi_connection->send_note_on(chan.as_int(), *note.into_midi_note(), vel.as_int()); } + return Value{}; } return Error { @@ -1027,6 +1403,12 @@ static Result builtin_note_on(Interpreter &interpreter, std::vector builtin_note_off(Interpreter &interpreter, std::vector args) { @@ -1043,6 +1425,7 @@ static Result builtin_note_off(Interpreter &interpreter, std::vectorfill(note); interpreter.midi_connection->send_note_off(chan.as_int(), *note.into_midi_note(), 127); } + return Value{}; } return Error { @@ -1058,6 +1441,13 @@ static Result builtin_note_off(Interpreter &interpreter, std::vector mix (a, b, c) (1, 2, 3) +//: (a, 1, b, 2, c, 3) +//: ``` /// Interleaves arguments static Result builtin_mix(Interpreter &i, std::vector args) { @@ -1089,6 +1479,90 @@ static Result builtin_mix(Interpreter &i, std::vector args) return result; } +inline void append_digits(std::vector &digits, usize base, Number number) +{ + auto start = digits.size(); + + number.simplify_inplace(); + + auto integer_part = number.num / number.den; + number.num -= integer_part * number.den; + + do { + digits.push_back(integer_part % base); + integer_part /= base; + } while (integer_part != 0); + std::reverse(digits.begin() + start, digits.end()); + + if (number.den != 1) { + std::unordered_set repeats; + + do { + repeats.insert(number); + number.num *= base; + + auto const digit = number.floor().as_int(); + digits.push_back(digit); + + number.num = number.num - digit * number.den; + number.simplify_inplace(); + } while (number.num != 0 && !repeats.contains(number)); + } +} + +//: Funkcja `digits` zamienia liczbę na listę jej cyfr. +//: +//: # Przykład +//: ``` +//: > digits 335 +//: (3, 3, 5) +//: ``` +/// Converts number to array of its digits +static Result builtin_digits(Interpreter &interpreter, std::vector args) +{ + // For now it's a constant. It waits on some kind of keyword parameters + // so base can be provided nicely + auto const base = 10; + + std::vector digits; + + auto args_flattened = Try(deep_flat(interpreter, args)); + for (auto const& arg : args_flattened) { + if (auto num = get_if(arg)) { + append_digits(digits, base, *num); + } else { + // TODO This may be an error. Currently we fail silently + continue; + } + } + + std::vector result; + result.reserve(digits.size()); + std::transform(digits.begin(), digits.end(), std::back_inserter(result), [](auto digit) { return Number(digit); }); + return result; +} + +//: Funkcja `call` wywołuje funkcje z pierwszego argumentu, aplikując pozostałe jej argumenty jako argumenty wywoływanej funkcji. +//: +//: # Przykład +//: ``` +//: > call digits 88976 +//: (8, 8, 9, 7, 6) +//: ``` +//: +//: Funkcja `call` umożliwia wywoływanie funkcji, które nie przyjmują żadnych argumentów. +//: Szczególnie przydatne jest to w przypadku funkcji `len`, `oct`, `bpm`, które zwracają +//: aktualną wartośc domyślną w przypadku braku argumentów +//: +//: # Przykład +//: ``` +//: > call bpm +//: 120 +//: > x := (say 42, say 43) +//: > call x +//: 42 +//: 43 +//: ``` /// Call operator. Calls first argument with remaining arguments static Result builtin_call(Interpreter &i, std::vector args) { @@ -1113,14 +1587,15 @@ void Interpreter::register_builtin_functions() { auto &global = *Env::global; - global.force_define("bpm", ctx_read_write_property<&Context::bpm>); + global.force_define("bpm", builtin_bpm); global.force_define("call", builtin_call); - global.force_define("ceil", apply_numeric_transform<&Number::ceil>); + global.force_define("ceil", builtin_ceil); global.force_define("chord", builtin_chord); - global.force_define("down", builtin_range); + global.force_define("digits", builtin_digits); + global.force_define("down", builtin_down); global.force_define("duration", builtin_duration); global.force_define("flat", builtin_flat); - global.force_define("floor", apply_numeric_transform<&Number::floor>); + global.force_define("floor", builtin_floor); global.force_define("fold", builtin_fold); global.force_define("for", builtin_for); global.force_define("hash", builtin_hash); @@ -1134,7 +1609,7 @@ void Interpreter::register_builtin_functions() global.force_define("note_off", builtin_note_off); global.force_define("note_on", builtin_note_on); global.force_define("nprimes", builtin_primes); - global.force_define("oct", ctx_read_write_property<&Context::octave>); + global.force_define("oct", builtin_oct); global.force_define("par", builtin_par); global.force_define("partition", builtin_partition); global.force_define("permute", builtin_permute); @@ -1142,10 +1617,10 @@ void Interpreter::register_builtin_functions() global.force_define("pick", builtin_pick); global.force_define("play", builtin_play); global.force_define("program_change", builtin_program_change); - global.force_define("range", builtin_range); + global.force_define("range", builtin_range); global.force_define("reverse", builtin_reverse); global.force_define("rotate", builtin_rotate); - global.force_define("round", apply_numeric_transform<&Number::round>); + global.force_define("round", builtin_round); global.force_define("scan", builtin_scan); global.force_define("set_len", builtin_set_len); global.force_define("set_oct", builtin_set_oct); @@ -1156,7 +1631,7 @@ void Interpreter::register_builtin_functions() global.force_define("typeof", builtin_typeof); global.force_define("uniq", builtin_uniq); global.force_define("unique", builtin_unique); - global.force_define("up", builtin_range); + global.force_define("up", builtin_up); global.force_define("update", builtin_update); global.force_define("while", builtin_while); } diff --git a/musique/interpreter/interpreter.cc b/musique/interpreter/interpreter.cc index ed4aa73..10ac1e8 100644 --- a/musique/interpreter/interpreter.cc +++ b/musique/interpreter/interpreter.cc @@ -91,11 +91,27 @@ Result Interpreter::eval(Ast &&ast) if (ast.token.source == "=") { auto lhs = std::move(ast.arguments.front()); auto rhs = std::move(ast.arguments.back()); - ensure(lhs.type == Ast::Type::Literal && lhs.token.type == Token::Type::Symbol, - "Currently LHS of assigment must be an identifier"); // TODO(assert) + + if (lhs.type != Ast::Type::Literal || lhs.token.type != Token::Type::Symbol) { + return Error { + .details = errors::Unsupported_Types_For { + .type = errors::Unsupported_Types_For::Operator, + .name = "=", + .possibilities = {}, + }, + .location = ast.token.location, + }; + } Value *v = env->find(std::string(lhs.token.source)); - ensure(v, "Cannot resolve variable: "s + std::string(lhs.token.source)); // TODO(assert) + if (v == nullptr) { + return Error { + .details = errors::Missing_Variable { + .name = std::string(lhs.token.source) + }, + .location = lhs.location, + }; + } return *v = Try(eval(std::move(rhs)).with_location(ast.token.location)); } @@ -119,7 +135,7 @@ Result Interpreter::eval(Ast &&ast) auto op = operators.find(std::string(ast.token.source.substr(0, ast.token.source.size()-1))); if (op == operators.end()) { return Error { - .details = errors::Undefined_Operator { .op = ast.token.source }, + .details = errors::Undefined_Operator { .op = std::string(ast.token.source) }, .location = ast.token.location }; } @@ -138,7 +154,7 @@ Result Interpreter::eval(Ast &&ast) } return Error { - .details = errors::Undefined_Operator { .op = ast.token.source }, + .details = errors::Undefined_Operator { .op = std::string(ast.token.source) }, .location = ast.token.location }; } diff --git a/musique/lexer/lexer.cc b/musique/lexer/lexer.cc index be90fc5..b91e322 100644 --- a/musique/lexer/lexer.cc +++ b/musique/lexer/lexer.cc @@ -105,7 +105,7 @@ auto Lexer::next_token() -> Result> // Lex numeric literals // They may have following forms: 0, 0.1 - if (consume_if(unicode::is_digit)) { + if (consume_if(unicode::is_digit) || consume_if('-', unicode::is_digit)) { while (consume_if(unicode::is_digit)) {} if (peek() == '.') { consume(); diff --git a/musique/parser/parser.cc b/musique/parser/parser.cc index 54129a9..af8e103 100644 --- a/musique/parser/parser.cc +++ b/musique/parser/parser.cc @@ -248,7 +248,7 @@ Result Parser::parse_atomic_expression() // So we need to explicitly allow only keywords that are also literals if (std::find(Literal_Keywords.begin(), Literal_Keywords.end(), peek()->source) == Literal_Keywords.end()) { return Error { - .details = errors::Unexpected_Keyword { .keyword = peek()->source }, + .details = errors::Unexpected_Keyword { .keyword = std::string(peek()->source) }, .location = peek()->location }; } @@ -313,8 +313,8 @@ Result Parser::parse_atomic_expression() if (success && invalid_token) { return Error { .details = errors::Literal_As_Identifier { - .type_name = type_name(invalid_token->type), - .source = invalid_token->source, + .type_name = std::string(type_name(invalid_token->type)), + .source = std::string(invalid_token->source), .context = "block parameter list" }, .location = invalid_token->location @@ -329,7 +329,7 @@ Result Parser::parse_atomic_expression() return Error { .details = errors::Wrong_Arity_Of { .type = errors::Wrong_Arity_Of::Operator, - .name = peek()->source, + .name = std::string(peek()->source), .expected_arity = 2, // TODO This should be resolved based on operator .actual_arity = 0, }, @@ -340,8 +340,8 @@ Result Parser::parse_atomic_expression() default: return Error { .details = errors::internal::Unexpected_Token { - .type = type_name(peek()->type), - .source = peek()->source, + .type = std::string(type_name(peek()->type)), + .source = std::string(peek()->source), .when = "atomic expression parsing" }, .location = peek()->location @@ -355,8 +355,8 @@ Result Parser::parse_identifier_with_trailing_separators() // TODO Specific error message return Error { .details = errors::internal::Unexpected_Token { - .type = type_name(peek()->type), - .source = peek()->source, + .type = std::string(type_name(peek()->type)), + .source = std::string(peek()->source), .when = "identifier parsing" }, .location = peek()->location @@ -373,8 +373,8 @@ Result Parser::parse_identifier() // TODO Specific error message return Error { .details = errors::internal::Unexpected_Token { - .type = type_name(peek()->type), - .source = peek()->source, + .type = std::string(type_name(peek()->type)), + .source = std::string(peek()->source), .when = "identifier parsing" }, .location = peek()->location diff --git a/musique/value/number.cc b/musique/value/number.cc index 3a5f505..ed90677 100644 --- a/musique/value/number.cc +++ b/musique/value/number.cc @@ -242,7 +242,7 @@ parse_fractional: .location = std::move(token.location) }; } - result += Number{ frac, pow10(frac_end - num_end) }; + result += Number{ (result.num < 0 ? -1 : 1) * frac, pow10(frac_end - num_end) }; } return result.simplify(); @@ -254,22 +254,30 @@ namespace impl static Number round(Number result, Rounding_Mode rm) { - if (result.den <= -1 || result.den >= 1) { - if (auto const r = result.num % result.den; r != 0) { - auto const sign = (result.num < 0) xor (result.den < 0) ? -1 : 1; - - if (rm == Rounding_Mode::Round) - rm = r * 2 >= result.den ? Rounding_Mode::Ceil : Rounding_Mode::Floor; - - if (rm == Rounding_Mode::Floor) result.num -= sign * r; - if (rm == Rounding_Mode::Ceil) result.num += sign * r; - - result.num /= result.den; - result.den = 1; - } else { - result.simplify_inplace(); - } + if (result.den == -1 || result.den == 1) { + return result.simplify(); } + auto const negative = (result.num < 0) xor (result.den < 0); + if (result.num < 0) result.num *= -1; + if (result.den < 0) result.den *= -1; + + if (auto const r = result.num % result.den; r != 0) { + bool ceil = rm == Rounding_Mode::Ceil; + ceil |= rm == Rounding_Mode::Round && (negative + ? r*2 <= result.den + : r*2 >= result.den); + + if (ceil ^ negative) { + result.num += result.den; + } + + // C++ integer division handles floor case + result.num /= result.den; + result.den = 1; + } else { + result.simplify_inplace(); + } + result.num *= negative ? -1 : 1; return result; } } diff --git a/musique/value/value.cc b/musique/value/value.cc index 8f12de2..c73b9eb 100644 --- a/musique/value/value.cc +++ b/musique/value/value.cc @@ -113,7 +113,7 @@ Result Value::operator()(Interpreter &i, std::vector args) const return (*func)(i, std::move(args)); } - return errors::Not_Callable { .type = type_name(*this) }; + return errors::Not_Callable { .type = std::string(type_name(*this)) }; } diff --git a/regression-tests/builtin/ceil.mq b/regression-tests/builtin/ceil.mq new file mode 100644 index 0000000..17eceb7 --- /dev/null +++ b/regression-tests/builtin/ceil.mq @@ -0,0 +1,11 @@ +say (ceil -4), +say (ceil -4.2), +say (ceil -4.5), +say (ceil -4.8), +say (ceil -5), + +say (ceil 4), +say (ceil 4.2), +say (ceil 4.5), +say (ceil 4.8), +say (ceil 5), diff --git a/regression-tests/builtin/digits.mq b/regression-tests/builtin/digits.mq new file mode 100644 index 0000000..044a033 --- /dev/null +++ b/regression-tests/builtin/digits.mq @@ -0,0 +1,8 @@ +say (digits 3456), +say (digits (100/10)), +say (digits 0), +say (digits (0 - 1234)), +say (digits 0 0 0 0), +say (digits (4/3)), +say (digits 0.5), +say (digits 1234.5678), diff --git a/regression-tests/builtin/duration.mq b/regression-tests/builtin/duration.mq new file mode 100644 index 0000000..28114de --- /dev/null +++ b/regression-tests/builtin/duration.mq @@ -0,0 +1,5 @@ +len (1/4), +say (duration (chord c e g)), +say (duration c), +say (duration (c hn e hn)), +say (duration (c 0.3)), diff --git a/regression-tests/builtin/floor.mq b/regression-tests/builtin/floor.mq new file mode 100644 index 0000000..f98c9d0 --- /dev/null +++ b/regression-tests/builtin/floor.mq @@ -0,0 +1,11 @@ +say (floor -4), +say (floor -4.2), +say (floor -4.5), +say (floor -4.8), +say (floor -5), + +say (floor 4), +say (floor 4.2), +say (floor 4.5), +say (floor 4.8), +say (floor 5), diff --git a/regression-tests/builtin/range.mq b/regression-tests/builtin/range.mq index 61ae424..656ddaf 100644 --- a/regression-tests/builtin/range.mq +++ b/regression-tests/builtin/range.mq @@ -1,17 +1,17 @@ say (range 0), -say (range (0 - 1)), +say (range -1), say (range 10), say (range 1 10), say (range 1 10 2), say (up 0), -say (up (0 - 1)), +say (up -1), say (up 10), say (up 1 10), say (up 1 10 2), say (down 0), -say (down (0 - 1)), +say (down -1), say (down 10), say (down 1 10), say (down 1 10 2), diff --git a/regression-tests/builtin/round.mq b/regression-tests/builtin/round.mq new file mode 100644 index 0000000..07eabf4 --- /dev/null +++ b/regression-tests/builtin/round.mq @@ -0,0 +1,11 @@ +say (round -4), +say (round -4.2), +say (round -4.5), +say (round -4.8), +say (round -5), + +say (round 4), +say (round 4.2), +say (round 4.5), +say (round 4.8), +say (round 5), diff --git a/regression-tests/test_db.json b/regression-tests/test_db.json index 89e6fd7..4ed1bcc 100644 --- a/regression-tests/test_db.json +++ b/regression-tests/test_db.json @@ -200,7 +200,89 @@ "10" ], "stderr_lines": [] + }, + { + "name": "digits.mq", + "exit_code": 0, + "stdin_lines": [], + "stdout_lines": [ + "(3, 4, 5, 6)", + "(1, 0)", + "(0)", + "(1, 8, 4, 4, 6, 7, 4, 4, 0, 7, 3, 7, 0, 9, 5, 5, 0, 3, 8, 2)", + "(0, 0, 0, 0)", + "(1, 3)", + "(0, 5)", + "(1, 2, 3, 4, 5, 6, 7, 8)" + ], + "stderr_lines": [] + }, + { + "name": "ceil.mq", + "exit_code": 0, + "stdin_lines": [], + "stdout_lines": [ + "-4", + "-4", + "-4", + "-4", + "-5", + "4", + "5", + "5", + "5", + "5" + ], + "stderr_lines": [] + }, + { + "name": "floor.mq", + "exit_code": 0, + "stdin_lines": [], + "stdout_lines": [ + "-4", + "-5", + "-5", + "-5", + "-5", + "4", + "4", + "4", + "4", + "5" + ], + "stderr_lines": [] + }, + { + "name": "round.mq", + "exit_code": 0, + "stdin_lines": [], + "stdout_lines": [ + "-4", + "-4", + "-4", + "-5", + "-5", + "4", + "4", + "5", + "5", + "5" + ], + "stderr_lines": [] + }, + { + "name": "duration.mq", + "exit_code": 0, + "stdin_lines": [], + "stdout_lines": [ + "3/4", + "1/4", + "1", + "3/10" + ], + "stderr_lines": [] } ] } -] +] \ No newline at end of file diff --git a/scripts/document-builtin.py b/scripts/document-builtin.py new file mode 100755 index 0000000..4a77f46 --- /dev/null +++ b/scripts/document-builtin.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +import argparse +import dataclasses +import string +import re +import itertools +import typing +import subprocess + +MARKDOWN_CONVERTER = "lowdown -m 'shiftheadinglevelby=3'" +CPP_FUNC_IDENT_ALLOWLIST = string.ascii_letters + string.digits + "_" +PROGRAM_NAME: str = "" + +HTML_PREFIX = """ + + + Dokumentacja funkcji języka Musique + + + +

Dokumentacja funkcji języka Musique

+""" + +HTML_SUFFIX = """ + + + +""" + + +def warning(*args, prefix: str | None = None): + if prefix is None: + prefix = PROGRAM_NAME + message = ": ".join(itertools.chain([prefix, "warning"], args)) + print(message) + + +def error(*args, prefix=None): + if prefix is None: + prefix = PROGRAM_NAME + message = ": ".join(itertools.chain([prefix, "error"], args)) + print(message) + exit(1) + + +@dataclasses.dataclass +class Builtin: + implementation: str + definition_location: tuple[str, int] # Filename and line number + names: list[str] + documentation: str + + +def builtins_from_file(source_path: str) -> typing.Generator[Builtin, None, None]: + with open(source_path) as f: + source = f.readlines() + + builtins: dict[str, Builtin] = {} + definition = re.compile( + r"""force_define.*\("([^"]+)"\s*,\s*(builtin_[a-zA-Z0-9_]+)\)""" + ) + + current_documentation = [] + + for lineno, line in enumerate(source): + line = line.strip() + + # Check if line contains force_define with static string and builtin_* + # thats beeing defined. It's a one of many names that given builtin + # has in Musique + if result := definition.search(line): + musique_name = result.group(1) + builtin_name = result.group(2) + + if builtin_name in builtins: + builtins[builtin_name].names.append(musique_name) + else: + error( + f"tried adding Musique name '{musique_name}' to builtin '{builtin_name}' that has not been defined yet", + prefix=f"{source_path}:{lineno}", + ) + continue + + # Check if line contains special documentation comment. + # We assume that only documentation comments are in given line (modulo whitespace) + if line.startswith("//:"): + line = line.removeprefix("//:").strip() + current_documentation.append(line) + continue + + # Check if line contains builtin_* identifier. + # If contains then this must be first definition of this function + # and therefore all documentation comments before it describe it + if (index := line.find("builtin_")) >= 0 and line[ + index - 1 + ] not in CPP_FUNC_IDENT_ALLOWLIST: + identifier = line[index:] + for i, char in enumerate(identifier): + if char not in CPP_FUNC_IDENT_ALLOWLIST: + identifier = identifier[:i] + break + + if identifier not in builtins: + builtin = Builtin( + implementation=identifier, + # TODO Allow redefinition of source path with some prefix + # to allow website links + definition_location=(source_path, lineno), + names=[], + documentation="\n".join(current_documentation), + ) + builtins[identifier] = builtin + current_documentation = [] + continue + + for builtin in builtins.values(): + builtin.names.sort() + yield builtin + + +def filter_builtins(builtins: list[Builtin]) -> typing.Generator[Builtin, None, None]: + for builtin in builtins: + if not builtin.documentation: + warning(f"builtin '{builtin.implementation}' doesn't have documentation") + continue + + # Testt if builtin is unused + if not builtin.names: + continue + + yield builtin + + +def each_musique_name_occurs_once( + builtins: typing.Iterable[Builtin], +) -> typing.Generator[Builtin, None, None]: + names = {} + for builtin in builtins: + for name in builtin.names: + if name in names: + error( + f"'{name}' has been registered as both '{builtin.implementation}' and '{names[name]}'" + ) + names[name] = builtin.implementation + yield builtin + + +def generate_html_document(builtins: list[Builtin], output_path: str): + with open(output_path, "w") as out: + out.write(HTML_PREFIX) + + out.write("") + + out.write("
") + + for builtin in builtins: + out.write("

") + out.write( + f"""

§{', '.join(builtin.names)}

""" + ) + out.write( + subprocess.check_output( + MARKDOWN_CONVERTER, + input=builtin.documentation, + encoding="utf-8", + shell=True, + ) + ) + out.write("

") + + out.write("
") + + out.write(HTML_SUFFIX) + + +def main(source_path: str, output_path: str): + "Generates documentaiton from file source_path and saves in output_path" + + builtins = builtins_from_file(source_path) + builtins = filter_builtins(builtins) + builtins = each_musique_name_occurs_once(builtins) + builtins = sorted(list(builtins), key=lambda builtin: builtin.names[0]) + + generate_html_document(builtins, output_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Builtin functions documentation generator from C++ Musique implementation" + ) + parser.add_argument( + "source", + type=str, + nargs=1, + help="C++ source file from which documentation will be generated", + ) + parser.add_argument( + "-o", + "--output", + type=str, + nargs=1, + required=True, + help="path for standalone HTML file containing generated documentation", + ) + + PROGRAM_NAME = parser.prog + args = parser.parse_args() + assert len(args.source) == 1 + assert len(args.output) == 1 + + main(source_path=args.source[0], output_path=args.output[0]) diff --git a/scripts/release b/scripts/release deleted file mode 100755 index 20e6a86..0000000 --- a/scripts/release +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -# This script creates a release of Musique programming language -# Release is defined as a zip archive containing source code, -# build binaries for supported platforms and build documentation - -set -e -o pipefail - -Suffix="$(date +"%Y-%m-%d")" -Target="release_$Suffix" -Image="musique-builder" - -if [ -d "$Target" ]; then - rm -rf "$Target" -fi - -mkdir -p "$Target" - -if [[ "$(docker images -q "$Image")" == "" ]]; then - docker build -t "$Image" . -fi -sudo rm -rf bin/ -docker run -it --rm -v "$(pwd):/musique" -w /musique "$Image" make os=linux CC=gcc-11 CXX=g++-11 >/dev/null - -cp bin/musique "$Target"/musique-x86_64-linux - -sudo rm -rf bin/ -make os=windows >/dev/null -cp bin/musique.exe "$Target"/musique-x86_64-windows.exe - -cp LICENSE "$Target"/LICENSE -cp CHANGELOG.md "$Target/CHANGELOG.md" -cp -r examples "$Target"/examples -sed "s/bin\/musique/musique/" scripts/install > "$Target"/install.sh -chmod 0755 "$Target"/install.sh - -lowdown -s doc/functions.md -m "title:Lista funkcji języka Musique" -o "$Target"/functions.html -python scripts/language-cmp-cheatsheet.py doc/musique-vs-languages-cheatsheet.template -mv doc/musique-vs-languages-cheatsheet.html "${Target}/musique-vs-others-cheatsheet.html" -make doc/wprowadzenie.html && mv doc/wprowadzenie.html "${Target}/wprowadzenie.html" - -git clone --recursive --quiet --depth=1 "$(git remote -v | awk '{ print $2 }' | head -n1)" "$Target"/source_code -rm -rf "$Target"/source_code/.git - -zip -q -r "musique_$Suffix.zip" "$Target"/* diff --git a/scripts/test.mk b/scripts/test.mk index 23df39a..3afa48c 100644 --- a/scripts/test.mk +++ b/scripts/test.mk @@ -1,3 +1,3 @@ -test: bin/debug/musique +test: bin/$(os)/debug/musique python3 scripts/test.py diff --git a/scripts/test.py b/scripts/test.py index 692e48b..9776699 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -5,6 +5,7 @@ import json import os import subprocess +TEST_DIR = "regression-tests" TEST_DB = "test_db.json" INTERPRETER = "bin/linux/debug/musique" @@ -94,7 +95,58 @@ class TestSuite: suites = list[TestSuite]() -def traverse(discover: bool, update: bool): +def suite_case_from_path(path: str) -> tuple[str, str]: + path = os.path.realpath(path) + test_suite, test_case = os.path.split(path) + test_dir, test_suite = os.path.split(test_suite) + _, test_dir = os.path.split(test_dir) + + assert test_dir == TEST_DIR, "Provided path doesn't follow required directory structure" + assert test_case.endswith(".mq"), "Test case is not a Musique file" + assert os.path.isfile(path), "Test case is not a file" + + return (test_suite, test_case) + +def add(path: str) -> list[tuple[TestSuite, TestCase]]: + test_suite, test_case = suite_case_from_path(path) + + for suite in suites: + if suite.name == test_suite: + break + else: + print(f"Discovered new test suite: {test_suite}") + suite = TestSuite(name=test_suite) + suites.append(suite) + + for case in suite.cases: + if case.name == test_case: + print(f"Test case {test_case} in suite {test_suite} already exists") + return [] + + case = TestCase(name=test_case) + suite.cases.append(case) + return [(suite, case)] + +def update(path: str) -> list[tuple[TestSuite, TestCase]]: + test_suite, test_case = suite_case_from_path(path) + + for suite in suites: + if suite.name == test_suite: + break + else: + print(f"Cannot update case {test_case} where suite {test_suite} was not defined yet.") + print("Use --add to add new test case") + return [] + + for case in suite.cases: + if case.name == test_case: + return [(suite, case)] + + print(f"Case {test_case} doesn't exists in suite {test_suite}") + print("Use --add to add new test case") + return [] + +def traverse(discover: bool, update: bool) -> list[tuple[TestSuite, TestCase]]: to_record = list[tuple[TestSuite, TestCase]]() if discover: for suite_name in os.listdir(testing_dir): @@ -114,16 +166,7 @@ def traverse(discover: bool, update: bool): if update: to_record.extend(((suite, case) for suite in suites for case in suite.cases)) - for (suite, case) in to_record: - case.record( - interpreter=os.path.join(root, INTERPRETER), - source=os.path.join(testing_dir, suite.name, case.name), - cwd=root - ) - - with open(test_db_path, "w") as f: - json_suites = [dataclasses.asdict(suite) for suite in suites] - json.dump(json_suites, f, indent=2) + return to_record def test(): successful, total = 0, 0 @@ -145,12 +188,14 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description="Regression test runner for Musique programming language") parser.add_argument("-d", "--discover", action="store_true", help="Discover all tests that are not in testing database") - parser.add_argument("-u", "--update", action="store_true", help="Update all tests") + parser.add_argument("--update-all", action="store_true", help="Update all tests", dest="update_all") + parser.add_argument("-a", "--add", action="append", help="Add new test to test suite", default=[]) + parser.add_argument("-u", "--update", action="append", help="Update test case", default=[]) args = parser.parse_args() root = os.path.dirname(os.path.dirname(__file__)) - testing_dir = os.path.join(root, "regression-tests") + testing_dir = os.path.join(root, TEST_DIR) test_db_path = os.path.join(testing_dir, TEST_DB) with open(test_db_path, "r") as f: @@ -158,7 +203,27 @@ if __name__ == "__main__": src["cases"] = [TestCase(**case) for case in src["cases"]] suites.append(TestSuite(**src)) - if args.discover or args.update: - traverse(discover=args.discover, update=args.update) - else: + to_record = [] + + if args.discover or args.update_all: + to_record.extend(traverse(discover=args.discover, update=args.update_all)) + elif not (args.add or args.update): test() + + for case in args.add: + to_record.extend(add(case)) + + for case in args.update: + to_record.extend(update(case)) + + for (suite, case) in to_record: + case.record( + interpreter=os.path.join(root, INTERPRETER), + source=os.path.join(testing_dir, suite.name, case.name), + cwd=root + ) + + if to_record: + with open(test_db_path, "w") as f: + json_suites = [dataclasses.asdict(suite) for suite in suites] + json.dump(json_suites, f, indent=2)