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を利用するくらいならばヘルパークラスを一つ用意することで解決するという方針をとっています。 ↩︎


Last modified January 10, 2021: fixed menuweights (e0c18df)