From bda1e503c7afa1ecb2a136e63abfb9475589e315 Mon Sep 17 00:00:00 2001
From: Robert Bendun
Date: Thu, 1 Dec 2022 00:52:53 +0100
Subject: [PATCH 01/33] Builtin function documentation generates from
implementation
---
CHANGELOG.md | 9 +
Makefile | 3 +
doc/functions.md | 115 ----------
musique/interpreter/builtin_functions.cc | 78 ++++++-
scripts/document-builtin.py | 256 +++++++++++++++++++++++
scripts/release | 45 ----
6 files changed, 336 insertions(+), 170 deletions(-)
delete mode 100644 doc/functions.md
create mode 100755 scripts/document-builtin.py
delete mode 100755 scripts/release
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c89cd75..e8bc40e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Printing version number on non-quiet launch, or when provided `--version` or `:version`
+- Builtin function documentation generation from C++ Musique implementation source code
+
+### Removed
+
+- Release builder, since it's separate part of the project
+
## [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/musique/interpreter/builtin_functions.cc b/musique/interpreter/builtin_functions.cc
index d92d5d0..c5b78b6 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)
{
@@ -172,12 +213,16 @@ invalid_argument_type:
};
}
+Forward_Implementation(builtin_ceil, apply_numeric_transform<&Number::ceil>)
+Forward_Implementation(builtin_floor, apply_numeric_transform<&Number::floor>)
+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);
@@ -210,7 +255,20 @@ static Result builtin_range(Interpreter&, std::vector args)
return array;
}
-/// Send MIDI Program Change message
+Forward_Implementation(builtin_range, range)
+Forward_Implementation(builtin_up, range)
+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;
@@ -1113,14 +1171,14 @@ 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("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 +1192,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 +1200,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 +1214,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/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("