Builtin function documentation generates from implementation

This commit is contained in:
Robert Bendun 2022-12-01 00:52:53 +01:00
parent b51088a9f0
commit bda1e503c7
6 changed files with 336 additions and 170 deletions

View File

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

View File

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

View File

@ -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: `(<litera_nuty> <numer_oktawy> <czas_trwania>)`:
- 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ść`;

View File

@ -11,6 +11,14 @@
#include <chrono>
#include <thread>
/// 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<Value> New_Function_Name(Interpreter &interpreter, std::vector<Value> args) \
{ \
return Implementation(interpreter, std::move(args)); \
}
/// Check if type has index method
template<typename T>
concept With_Index_Method = requires (T &t, Interpreter interpreter, usize position) {
@ -103,6 +111,39 @@ static Result<Value> 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<Array> into_flat_array(Interpreter &interpreter, std::span<Value> 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<Range_Direction dir>
static Result<Value> builtin_range(Interpreter&, std::vector<Value> args)
static Result<Value> range(Interpreter&, std::vector<Value> args)
{
auto start = Number(0), stop = Number(0), step = Number(1);
@ -210,7 +255,20 @@ static Result<Value> builtin_range(Interpreter&, std::vector<Value> args)
return array;
}
/// Send MIDI Program Change message
Forward_Implementation(builtin_range, range<Range_Direction::Up>)
Forward_Implementation(builtin_up, range<Range_Direction::Up>)
Forward_Implementation(builtin_down, range<Range_Direction::Down>)
//: 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<Value> args) -> Result<Value> {
if (auto a = match<Number>(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<Range_Direction::Down>);
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<Range_Direction::Up>);
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<Range_Direction::Up>);
global.force_define("up", builtin_up);
global.force_define("update", builtin_update);
global.force_define("while", builtin_while);
}

256
scripts/document-builtin.py Executable file
View File

@ -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 = """<html>
<head>
<meta charset="utf-8">
<title>Dokumentacja funkcji języka Musique</title>
<style>
html {
color: #1a1a1a;
background-color: #fdfdfd;
}
body {
font-family: sans-serif;
margin: 10px auto;
overflow-wrap: break-word;
text-rendering: optimizeLegibility;
font-kerning: normal;
max-width: 50em;
}
nav a, nav a:link, nav a:visited {
color: #07a;
text-decoration: none;
}
h2 > a, h2>a:visited, h2>a:link {
color: gray;
text-decoration: none;
padding-right: 5px;
}
h2 > a:hover { color: lightgrey; }
footer {
width: 100%;
border-top: 1px solid black;
}
</style>
</head>
<body>
<h1>Dokumentacja funkcji języka Musique</h1>
"""
HTML_SUFFIX = """
<footer>
Wszystkie treści podlegają licencji <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
</footer>
</body>
</html>
"""
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("<nav>")
names = sorted(
[
(name, builtin.implementation)
for builtin in builtins
for name in builtin.names
]
)
out.write(", ".join(f"""<a href="#{id}">{name}</a>""" for name, id in names))
out.write("</nav>")
out.write("<main>")
for builtin in builtins:
out.write("<p>")
out.write(
f"""<h2 id="{builtin.implementation}"><a href="#{builtin.implementation}">&sect;</a>{', '.join(builtin.names)}</h2>"""
)
out.write(
subprocess.check_output(
MARKDOWN_CONVERTER,
input=builtin.documentation,
encoding="utf-8",
shell=True,
)
)
out.write("</p>")
out.write("</main>")
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])

View File

@ -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"/*