This is the multi-page printable view of this section. Click here to print.
Developers Guide
1 - Setup development environments
Dependencies
- cmake
- bison 3.3~
- flex
- llvm 11~
- libsndfile
- rtaudio(cmake downloads automatically)
Optionally, Ninja is recommended for a fast build.
macOS
Install Xcode, and Xcode command line tools with the following command.
xcode-select --install
Install Homebrew by the instruction on the website.
Install dependencies with the following command.
brew tap mimium-org/mimium
brew install mimium -s --only-dependencies
Linux(Ubuntu)
If you are using Homebrew on Linux, you can use same installation command in the section of macOS.
If you want to install dependencies with apt, use the following command1.
pushd /tmp && wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && sudo ./llvm.sh && popd
sudo apt-get install libalsa-ocaml-dev libfl-dev libbison-dev libz-dev libvorbis-dev libsndfile-dev libopus-dev gcc-9 ninja-build
Windows (on MSYS2)
Currently, build on Windows is realized by using MSYS2(https://www.msys2.org/). Follow the instruction on MSYS2 website to install it.
Open MSYS MinGW64 terminal and install dependencies with the following command..
pacman -Syu --noconfirm git flex bison mingw-w64-x86_64-cmake mingw-w64-x86_64-gcc mingw64/mingw-w64-x86_64-libsndfile mingw64/mingw-w64-x86_64-opus mingw-w64-x86_64-ninja mingw-w64-x86_64-llvm
Clone the repository
Clone the repository of mimium with the git command.
git clone --recursive https://github.com/mimium-org/mimium.git
Editor
For the development of mimium, using Visual Studio Code is recommended.
Open mimium.code-workspace
with VSCode.
Recommended Extensions
When you open the workspace, the pop-up menu will be shown to install recommended extensions if they are not installed.
- cmake-tools
- clangd
- CodeLLDB
- Coverage Gutter
Especially, CMake Tools is necessary to develop C++ project with VSCode.
Configure CMake Kit
When you open the workspace with the Cmake Tools installed, you will be asked which CMake kit you want to use (only at the first time).
If you are on macOS, choose /usr/bin/clang
. Otherwise, choose an appropriate compiler you installsed, for example, /usr/local/bin/g++
on Linux, and /mingw64/bin/g++
on MSYS2.
Build
Select the CMake Tools tab from the menu on the left side of VS Code, and select Configure All. A build directory will be created under build/
(RtAudio will be downloaded and built automatically at this time).
Right next to Configure All, press Build All to start building all projects.
Build Targets
src/mimium_exe
: Builds the mainmimium
command.src/mimium
: Builds the main body of the library,libmimium
.test/Tests
: The target to build all tests (except Fuzzing).Lcov
LcovResetCounter
: Target available when-DENABLE_COVERAGE
is enabled. run theLcov
target after building and running one or more tests, etc. to collect coverage information. If you selectShow Coverage
from the Cmd+Shift+P command palette in this state, the actual execution of the code will be highlighted. After editing the source and rebuilding, you will need to runLcovResetCounter
because of coverage information conflicts.
Debugging on VSCode
Select the Run tab from the menu on the left side of VS Code. Configurations can be selected from next to the Run button. There are two workspaces: one to launch the CMake target (“CMake Target Debug(Workspace)”), and another to run it with command line options (“Launch with Arg(edit this config)(workspace )”).
You can choose which CMake target to launch by clicking “Select the target to launch” in the bottom menu bar.
If you want to launch by passing options to the command line, select “Launch with Arg(edit this config)(workspace)” and then use the gear symbol to directly edit the mimium.code-workspace file. For example, use “args” to specify the file name as shown in the following example.
...
{
"name": "Launch with arg(edit this config)",
"type": "lldb",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": ["${workspaceFolder}/examples/gain.mmm"],
"cwd": "${workspaceFolder}/build",
},
...
You can also pass the input from stdio by specifying like "stdio": ["${workspaceFolder}/stdin.txt",null,null],
.
Test
There are three main types of tests, using Google Test.
Unit tests
This is mainly a test to see if each section of the compiler works correctly by starting it alone.
Regression Test.
This is a test to invoke the actual built mimium
binary from the std::system
function and check if there are any errors in other sections due to the addition of new features.
Since mimium does not currently have a syntax for verifying tests, we pipe the calculation results to standard output using the println
function, and Google Test captures it to verify that the answer is correct.
Also, the test which launches the audio driver is currently not implemented.
Fuzzing test
This is a fuzzing test that uses Clang’s libfuzzer. Fuzzing tests are used to verify that a random input (in this case, a string of source code) is given in a gradually changing manner, and that if the syntax is correct, it will be processed correctly, and if it is an error, it will be treated as an error and an unexpected crash will not occur.
It is only validated on macOS and is not included in CI.
Run Ctest
You can execute test command by running ctest
command on build
directory, or you can execute Unit Test and Regressin test from the menu on right-bottom of VSCode.
On Linux(Ubuntu), we recommend installing llvm using an automatic installation script in https://apt.llvm.org/ because
llvm
package in apt does not containPolly
libs required byllvm-config --libs
. ref ↩︎
2 - Coding Style
Basic Policy
- language specification (LS) conforms to C++17.The main reason is why Code Template Inference improve readability such as
if constexpr
beacuse mimium use activelystd::variant
orstd::optional
. - **If you are uncertain about readability or a slight improvement in execution speed, take readability.**In the first place C++ is guranteerd to be reasonably fast, so it doesn’t matter if it can be a little rich processing, that degree of hesitation is about same when optimized.
- Avoid using external libraries as much as possible(especialy for general purpose such as boost).And STL is actively used.
- The compiler depends on bison(yacc) and flex(lex) as parser.This is especially valuable as bison source document,so we plan to continue using in the future, but there are problems Unicode not being able to be loaded for flex, I may move to REflex etc. or switch to manual implementation.Under consideration.
- The runtime currently depends on libsndfile etc. for reading audio file, but we would like to separate it as a project structure later because it is only needed at runtime.
- Mimium does not use raw pointers.Basically mimium uses
std::shared_ptr<T>
.Butllvm::Type*
in the LLVM library andllvm::Value*
are not limited to this because they implement their own reference counting. - In the conditional expression(e.g.if block),mimium does not use the fact the pointer variable is empty with
nullptr
.Even if it takes some effort,the document value of the source code itself is improved by usingstd::optional<std::shared_ptr<T>>
.
Dynamic Polymorphism
There are two types of polymorphism that switches indivisual processing for each type in C++: Static Polymorphism, which determines the type at compile time, and Dynamic Polymorphism, which determines the operation at runtime. Static Polymorphism is mainly implemented by templates, and Dynamic Polymorphism is mainly implemented by inheritance and virtual function.
However, the mimium development basically does not use virtual function for Dynamic Polymorphism.Instead, use std::variant
introduced in STL since C++17.std::variant<T1,T2,T3...>
is a type to which a variable having any of multiple types T1 to Tn can be substituted, and by using std::get<T>
and std::visit()
, it is possible to dynamically divide the processing according to the type.This is a substitute for the type called sum type that is often seen in functional types, and std::visit
enables processing close to so-called pattern matching when combined processing division using templates and constexpr.In internal implementation, it is maximum memory of the type that can be taken and securing a tag (integer) of which type is currently held, so it is also called Tagged Union.
mimiumにおける具体的な型でいうと抽象構文木であるmimium::ast::Expr
やmimium::ast::Statement
、中間表現であるmimium::mir::Instruction
、(mimium言語における)型を表すmimium::types::Value
などがstd::variant
へのエイリアスです。
仮想関数よりもvariant
を用いるメリットはなんでしょうか?1つは、実行コストがかかることです。仮想関数は実行時に関数へのテーブルを保持し仮想関数が呼び出されるたびにそれを参照する必要があるため、std::variant
を用いた多相の方が実行速度では一般的に有利だとされています。
もう1つはアップキャストの問題です。抽象構文木のような木構造のデータを渡りながら処理をする時、どうしても
- 継承した個別の型を基底クラスのポインターでダウンキャストして受け取る
- 仮想関数を用いて型に応じた処理をする
- 処理したあと帰ってきたポインターを元の型に戻し(アップキャスト)、さらに処理を続ける
といったパターンが発生します。このアップキャストは一般的なコーディングでは、間違った型へキャストして終えば予測不可能な挙動が起きるので御法度とされています。仮想関数で用いている実行時型情報(RTTI)を用いるdynamic_cast
を使って動的に型検査をして安全にアップキャストする方法もありますが、記述が長くなりがちなども問題もあります。一方でstd:variant
を使用するとこのようなダウン→アップキャストの必要はないので型情報が明確に取り扱えます。
またこうした(型に応じた処理を複数種類)x(複数の型)と組み合わせる方法はデザインパターンの中ではビジターパターンとも呼ばれ、 std::visit(function_object,variant_variable)
ではこの形が引数としてシンプルに表されています。一方仮想関数を使ってのビジターパターンはデータ側にacceptと呼ばれるメソッドを実装しておく必要があるので、データはデータ、関数は関数というように構造を分離することが難しくなります。
再帰的データ構造
std::variant
にも難しい点はあります。そのうち重要な点は再帰的データ構造がそのままでは扱えないことです。1
たとえば、types::Value
はtypes::Float
やtypes::Function
など取りうる型すべてを含んでいますが、ここでtypes::Function
のメンバ変数にはたとえば返り値を表す型としてtypes::Value
が含まれてしまっています。
std::variant
は通常の数値型のデータなどと同じように、取りうる型の最大値分だけメモリをスタック確保し、ヒープアロケーションは行わない仕様となっており、再帰的なデータ構造の場合はデータサイズを静的に決定できなくなってしまうのでコンパイルできなくなります。
これを回避するためには、再帰的な部分を持つデータについては実体の代わりにポインタを格納するなどの方法が考えられるのですが、初期化やメンバアクセスなどが統一されないためややこしくなるなどの問題があるため、mimiumでは次の記事を参考にしたヘルパークラスを利用しています。
具体的には内部のT
の実体を要素数1のstd::vector<T>
に確保し、キャスト演算子としてT& ()
を実装しimplicitに元の型から構築、キャストして参照を受け取ることができるようにしています。std::vector
はT
の中身が不完全なままでもスタック上のデータサイズを確定させることができるのでコンパイルが通るようになるのです。
記事中のrecursive_wrapper<T>
を、mimium内部ではRec_Wrap<T>
と名付け、さらにこの型をrT
(たとえばFunction
に対してrFunction
といった形で)エイリアスしています。
using Value = std::variant<None, Void, Float, String, rRef, rTypeVar, rPointer, rFunction,rClosure, rArray, rStruct, rTuple, rAlias>;
/*~~~~~*/
using rFunction = Rec_Wrap<Function>;
/*~~~~~*/
struct Function : Aggregate {
Value ret_type;
std::vector<Value> arg_types;
};
たとえばstd::visit
でパターンマッチングする時にはビジターの関数オブジェクトのoperator()
オーバーロードを以下のようにします。
types::Value operator()(types::Function& f){
return someProcess(f);
}
/*~~~~~*/
template<typename T>
types::Value operator()(Rec_Wrap<T>& t){
return (*this)(static_cast<T&>(t));
}
Rec_Wrap<T>
を一度キャストして剥がしてもう一度自分自身を適用するテンプレート関数を使います。operator()(Rec_Wrap<types::Function>& rf)
を直接オーバーロードしても構わないのですが、このrf
は直接メンバアクセスができないため一度ローカル変数でtypes::Function&
にキャストしてやらないといけなかったりする二度手間が発生します。
エラー処理、例外
try
、throw
、catch
と言ったC++標準のエラー処理を積極的に利用します。throw
を使ったエラー処理は実行コストが高いというデメリットがあるものの、正常系で処理をしている間はコストがかからず、言語組み込みの仕様なのでエラー処理部の可読性が高まります。
optionalなどを活用したエラー処理クラスの実装は型シグネチャが複雑になり可読性が下がるなどのデメリットもあります。ただ内部的に使用しているLLVMのライブラリでは実行速度を重視してExpected
クラスなどでこの形式でエラー処理をしているため、こちらとtry
、catch
のエラー処理を混ぜるのはあまり効率がよくはない、混乱するといった問題点も存在しています。
現状ではコンパイラもランタイムもtry/catchのエラー処理を使用していますが、ランタイム側では実行時間にセンシティブである、今後組み込み向けなどに移植される可能性もあると言った事情を考えるとoptional等を活用した処理に変更するかもしれません。
基本的にtry
,catch
はコンパイラでの処理のはじめに行い深くネストすることはしません。どの道、ほとんどのケースでエラーが起きれば最終的にコンパイルエラーの形に帰着するためです。必要に応じてエラークラスを継承して種類分けし、catchしたところでまとめてエラーの型ごとにメッセージ等を個別に処理します。( エラークラスの定義は今後の課題)
std::variantの実装の元となったBoostには再帰データを扱えるrecursive_variantが存在していますが、今のところそのためだけにboostを利用するくらいならばヘルパークラスを一つ用意することで解決するという方針をとっています。 ↩︎