This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Developers Guide

Overall structure, C++ coding style and guideline for compiler&runtime development.

Development

1 - Setup development environments

How to install dependency libraries, build source codes, debug and test.

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.

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 main mimium 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 the Lcov target after building and running one or more tests, etc. to collect coverage information. If you select Show 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 run LcovResetCounter 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.


  1. On Linux(Ubuntu), we recommend installing llvm using an automatic installation script in https://apt.llvm.org/ because llvm package in apt does not contain Polly libs required by llvm-config --libs. ref ↩︎

2 - Coding Style

Coding styles used in a development of mimium

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 actively std::variant or std::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>.But llvm::Type* in the LLVM library and llvm::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 using std::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::Exprmimium::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::Valuetypes::Floattypes::Functionなど取りうる型すべてを含んでいますが、ここでtypes::Functionのメンバ変数にはたとえば返り値を表す型としてtypes::Valueが含まれてしまっています。

std::variantは通常の数値型のデータなどと同じように、取りうる型の最大値分だけメモリをスタック確保し、ヒープアロケーションは行わない仕様となっており、再帰的なデータ構造の場合はデータサイズを静的に決定できなくなってしまうのでコンパイルできなくなります。

これを回避するためには、再帰的な部分を持つデータについては実体の代わりにポインタを格納するなどの方法が考えられるのですが、初期化やメンバアクセスなどが統一されないためややこしくなるなどの問題があるため、mimiumでは次の記事を参考にしたヘルパークラスを利用しています。

Breaking Circular Dependencies in Recursive Union Types With C++17 - Don’t Compute In Public(last view:2020-08-17)

具体的には内部のTの実体を要素数1のstd::vector<T>に確保し、キャスト演算子としてT& ()を実装しimplicitに元の型から構築、キャストして参照を受け取ることができるようにしています。std::vectorTの中身が不完全なままでもスタック上のデータサイズを確定させることができるのでコンパイルが通るようになるのです。

記事中の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&にキャストしてやらないといけなかったりする二度手間が発生します。

エラー処理、例外

trythrowcatchと言ったC++標準のエラー処理を積極的に利用します。throwを使ったエラー処理は実行コストが高いというデメリットがあるものの、正常系で処理をしている間はコストがかからず、言語組み込みの仕様なのでエラー処理部の可読性が高まります。

optionalなどを活用したエラー処理クラスの実装は型シグネチャが複雑になり可読性が下がるなどのデメリットもあります。ただ内部的に使用しているLLVMのライブラリでは実行速度を重視してExpectedクラスなどでこの形式でエラー処理をしているため、こちらとtrycatchのエラー処理を混ぜるのはあまり効率がよくはない、混乱するといった問題点も存在しています。

現状ではコンパイラもランタイムもtry/catchのエラー処理を使用していますが、ランタイム側では実行時間にセンシティブである、今後組み込み向けなどに移植される可能性もあると言った事情を考えるとoptional等を活用した処理に変更するかもしれません。

基本的にtry,catchはコンパイラでの処理のはじめに行い深くネストすることはしません。どの道、ほとんどのケースでエラーが起きれば最終的にコンパイルエラーの形に帰着するためです。必要に応じてエラークラスを継承して種類分けし、catchしたところでまとめてエラーの型ごとにメッセージ等を個別に処理します。( エラークラスの定義は今後の課題)


  1. std::variantの実装の元となったBoostには再帰データを扱えるrecursive_variantが存在していますが、今のところそのためだけにboostを利用するくらいならばヘルパークラスを一つ用意することで解決するという方針をとっています。 ↩︎