シリーズのパート3では、軽量プログラミング言語がついに実行されます。チューリング完全ではなく、強力ではありませんが、式を評価したり、C ++で記述された外部関数を呼び出したりすることもできます。
主にこのブログシリーズの目的であるため、またこの部分では物事が少し複雑になったため、私自身のドキュメントのために、プロセスをできるだけ詳細に説明しようと思います。
2番目の記事が公開される前にこの部分のコーディングを開始しましたが、式パーサーは独自のブログ投稿に値するスタンドアロンコンポーネントである必要があることがわかりました。
それは、いくつかの悪名高いプログラミング手法とともに、この部分がそれほど大きくないことを可能にしましたが、それでも、一部の読者は、おそらく前述のプログラミング手法を指摘し、なぜ私がそれらを使用しなければならなかったのか疑問に思います。
さまざまなプロジェクトやさまざまな人々とのプログラミング経験を積むにつれて、開発者はかなり独断的である傾向があることを学びました。おそらくその方が簡単だからです。
プログラミングの最初の教義は、goto
ということです。声明は悪く、邪悪で、恐ろしいものです。私はその感情がどこから来ているのか理解でき、誰かがgoto
を使用するほとんどの場合その概念に同意しますステートメント。通常は回避でき、代わりにもっと読みやすいコードを書くことができます。
ただし、C ++の内部ループからの離脱はgoto
で簡単に実行できることを否定することはできません。ステートメント。代替案— bool
が必要です変数または専用関数-禁止されているプログラミング手法のバケツに独断的に分類されるコードよりも読みにくくなる可能性があります。
Cとにのみ関連する2番目の教義 C ++開発者 、マクロは悪い、悪い、ひどい、そして基本的には災害が起こるのを待っているということです。これには、ほとんどの場合、次の例が伴います。
#define max(a, b) ((a) > (b) ? (a) : (b)) ... int x = 3; int z = 2; int y = max(x++, z);
そして、質問があります:x
の値は何ですかこのコードの後、答えは5
です。なぜならx
?
-演算子の両側に1つずつ、合計2回インクリメントされます。
唯一の問題は、このシナリオでは誰もマクロを使用しないことです。マクロは、通常の関数が正常に機能するシナリオで使用される場合、特に関数のふりをする場合は悪であるため、ユーザーはその副作用に気づきません。ただし、これらを関数として使用することはなく、名前にブロック文字を使用して、関数ではないことを明確にします。それらを適切にデバッグすることはできません。それは悪いことですが、同じコードを何十回もコピーして貼り付ける方法があり、マクロよりもエラーが発生しやすいため、これで問題は解決します。この問題の解決策の1つは、コードジェネレーターを作成することですが、C ++に既に組み込まれているのに、なぜコードジェネレーターを作成する必要があるのでしょうか。
プログラミングの教義はほとんどの場合悪いです。ここでは、設定したばかりのドグマトラップに再帰的に陥らないようにするために、「ほぼ」を慎重に使用しています。
この部分のコードとすべてのマクロを見つけることができます ここに 。
の中に 前の部分 、Storkはバイナリなどのアセンブリ言語にコンパイルされないことを述べましたが、静的に型指定された言語になることも述べました。したがって、コンパイルされますが、実行できるC ++オブジェクトになります。後で明らかになりますが、今のところ、すべての変数がそれ自体でオブジェクトになることを述べておきましょう。
それらをグローバル変数コンテナまたはスタックに保持したいので、1つの便利なアプローチは、基本クラスを定義してそれから継承することです。
class variable; using variable_ptr = std::shared_ptr; class variable: public std::enable_shared_from_this { private: variable(const variable&) = delete; void operator=(const variable&) = delete; protected: variable() = default; public: virtual ~variable() = default; virtual variable_ptr clone() const = 0; template T static_pointer_downcast() { return std::static_pointer_cast(shared_from_this()); } };
ご覧のとおり、これはかなり単純であり、ディープコピーを実行する関数clone
は、デストラクタを除く唯一の仮想メンバー関数です。
このクラスのオブジェクトは常にshared_ptr
を介して使用するため、std::enable_shared_from_this
から継承して、共有ポインターを簡単に取得できるようにすることは理にかなっています。関数static_pointer_downcast
このクラスからその実装に頻繁にダウンキャストする必要があるため、ここでは便宜上ここにあります。
C ++でコンパイルまたは解釈されます
このクラスの実際の実装はvariable_impl
であり、保持する型でパラメーター化されます。これは、使用する4つのタイプに対してインスタンス化されます。
using number = double; using string = std::shared_ptr; using array = std::deque; using function = std::function;
double
を使用します私たちの番号タイプとして。文字列は不変であるため、値で渡すときに特定の最適化を可能にするために、参照カウントされます。配列は安定しているためstd::deque
になりますが、runtime_context
に注意してください。実行時にプログラムメモリに関するすべての関連情報を保持するクラスです。これについては後で説明します。
次の定義も頻繁に使用されます。
using lvalue = variable_ptr; using lnumber = std::shared_ptr; using lstring = std::shared_ptr; using larray = std::shared_ptr; using lfunction = std::shared_ptr;
ここで使用されている「l」は「lvalue」の短縮形です。あるタイプの左辺値があるときはいつでも、variable_impl
への共有ポインターを使用します。
実行時、メモリ状態はクラスruntime_context
に保持されます。
class runtime_context{ private: std::vector _globals; std::deque _stack; std::stack _retval_idx; public: runtime_context(size_t globals); variable_ptr& global(int idx); variable_ptr& retval(); variable_ptr& local(int idx); void push(variable_ptr v); void end_scope(size_t scope_vars); void call(); variable_ptr end_function(size_t params); };
グローバル変数の数で初期化されます。
_globals
すべてのグローバル変数を保持します。それらはメンバー関数global
でアクセスされます絶対インデックス付き。_stack
ローカル変数と関数の引数、および整数を_retval_idx
の先頭に保持します絶対インデックスを_stack
に保持します現在の戻り値の。retval
でアクセスされ、ローカル変数と関数引数は関数local
でアクセスされます。現在の戻り値に関連するインデックスを渡すことによって。この場合、関数の引数には負のインデックスがあります。push
関数は変数をスタックに追加しますが、end_scope
渡された数の変数をスタックから削除します。call
関数はスタックのサイズを1つ変更し、_stack
の最後の要素のインデックスをプッシュします_retval_idx
へ。end_function
スタックから戻り値と渡された引数の数を削除し、削除された戻り値も返します。ご覧のとおり、低レベルのメモリ管理は実装せず、当然のことと思われるネイティブ(C ++)メモリ管理を利用します。少なくとも今のところ、ヒープ割り当ても実装しません。
runtime_context
を使用すると、この部分の中心的で最も難しいコンポーネントに必要なすべてのビルディングブロックがついに完成します。
ここで紹介する複雑な解決策を完全に説明するために、このアプローチを決定する前に失敗したいくつかの試みを簡単に紹介します。
最も簡単なアプローチは、すべての式をvariable_ptr
として評価することです。そして、この仮想基本クラスがあります:
class expression { ... public: variable_ptr evaluate(runtime_context& context) const = 0; lnumber evaluate_lnumber(runtime_context& context) const { return evaluate(context)->static_pointer_downcast(); } lstring evaluate_lstring(runtime_context& context) const { return evaluate(context)->static_pointer_downcast(); } number evaluate_number(runtime_context& context) const { return evaluate_lnumber(context)->value; } string evaluate_string(runtime_context& context) const { return evaluate_lstring(context)->value; } ... }; using expression_ptr = std::unique_ptr;
次に、加算、連結、関数呼び出しなどの各操作について、このクラスから継承します。たとえば、これは加算式の実装になります。
class add_expression: public expression { private: expression_ptr _expr1; expression_ptr _expr2; public: ... variable_ptr evaluate(runtime_context& context) const override{ return std::make_shared( _expr1->evaluate_number(context) + _expr2->evaluate_number(context) ); } ... };
したがって、両側(_expr1
と_expr2
)を評価し、それらを追加してから、variable_impl
を作成する必要があります。
コンパイル時に変数の型をチェックしたため、変数を安全にダウンキャストできます。これはここでは問題ではありません。ただし、大きな問題は、返されるオブジェクトのヒープ割り当てに対して支払うパフォーマンスの低下です。これは、理論的には必要ありません。仮想関数宣言を満たすためにこれを行っています。 Storkの最初のバージョンでは、関数から数値を返すときにそのペナルティがあります。私はそれで生きることはできますが、ヒープ割り当てを行う単純なプリインクリメント式では生きられません。
C ++で行う楽しいこと
次に、共通ベースから継承されたタイプ固有の式を試してみました。
class expression { ... public: virtual void evaluate(runtime_context& context) const = 0; ... }; class lvalue_expression: public virtual expression { ... public: virtual lvalue evaluate_lvalue(runtime_context& context) const = 0; void evaluate(runtime_context& context) const override { evaluate_lvalue(context); } ... }; using lvalue_expression_ptr = std::unique_ptr; class number_expression: public virtual expression { ... public: virtual number evaluate_number(runtime_context& context) const = 0; void evaluate(runtime_context& context) const override { evaluate_number(context); } ... }; using number_expression_ptr = std::unique_ptr; class lnumber_expression: public lvalue_expression, public number_expression { ... public: virtual lnumber evaluate_lnumber(runtime_context& context) const = 0; lvalue evaluate_lvalue(runtime_context& context) const override { return evaluate_lnumber(context); } number evaluate_number(runtime_context& context) const override { return evaluate_lnumber(context)->value; } void evaluate(runtime_context& context) const override { return evaluate_lnumber(context); } ... }; using lnumber_expression_ptr = std::unique_ptr;
これは階層の一部にすぎず(数値のみ)、すでにひし形の問題(同じ基本クラスを持つ2つのクラスを継承するクラス)に遭遇しました。
幸いなことに、C ++は仮想継承を提供します。これにより、継承されたクラスで基本クラスへのポインターを保持することにより、基本クラスから継承することができます。したがって、クラスBとCが実質的にAから継承し、クラスDがBとCから継承する場合、DにはAのコピーが1つだけ存在します。
その場合、支払う必要のあるペナルティがいくつかあります。たとえば、パフォーマンスとAからダウンキャストできないことなどですが、それでも、仮想継承を初めて使用する機会のように見えました。私の人生。
これで、加算式の実装がより自然になります。
class add_expression: public number_expression { private: number_expression_ptr _expr1; number_expression_ptr _expr2; public: ... number evaluate_number(runtime_context& context) const override{ return _expr1->evaluate_number(context) + _expr2->evaluate_number(context); } ... };
構文的には、これ以上求めるものはありません。これは当然のことです。ただし、内部式のいずれかが左辺値式である場合、それを評価するには2つの仮想関数呼び出しが必要になります。完璧ではありませんが、ひどいことでもありません。
このミックスに文字列を追加して、それがどこに到達するかを見てみましょう。
class string_expression: public virtual expression { ... public: virtual string evaluate_string(runtime_context& context) const = 0; void evaluate(runtime_context& context) const override { evaluate_string(context); } ... }; using string_expression_ptr = std::unique_ptr;
数値を文字列に変換できるようにしたいので、number_expression
を継承する必要がありますstring_expression
から。
class number_expression: public string_expression { ... public: virtual number evaluate_number(runtime_context& context) const = 0; string evaluate_string(runtime_context& context) const override { return tostring(evaluate_number(context)); } void evaluate(runtime_context& context) const override { evaluate_number(context); } ... }; using number_expression_ptr = std::unique_ptr;
私たちはそれを生き延びましたが、evaluate
を再度オーバーライドする必要があります仮想メソッド、または数値から文字列への不必要な変換により、深刻なパフォーマンスの問題に直面します。
そのため、物事は明らかに醜くなりつつあり、相互に変換する必要のある2種類の式がないため、私たちのデザインはほとんど生き残っていません(両方の方法)。その場合、または何らかの循環変換を試みた場合、階層はそれを処理できませんでした。結局のところ、階層は、より弱い関係に変換可能ではなく、関係を反映する必要があります。
ヒューリスティック分析とは
これらすべての失敗した試みは、私を複雑でありながら-私の意見では-適切な設計に導きました。まず、単一の基本クラスを持つことは私たちにとって重要ではありません。 voidと評価される式クラスが必要ですが、コンパイル時にvoid式と別の種類の式を区別できる場合は、実行時にそれらを変換する必要はありません。したがって、式の戻り値の型を使用して基本クラスをパラメーター化します。
そのクラスの完全な実装は次のとおりです。
template class expression { expression(const expression&) = delete; void operator=(const expression&) = delete; protected: expression() = default; public: using ptr = std::unique_ptr; virtual R evaluate(runtime_context& context) const = 0; virtual ~expression() = default; };
式の評価ごとに1つの仮想関数呼び出しのみがあり(もちろん、再帰的に呼び出す必要があります)、バイナリコードにコンパイルしないため、非常に良い結果が得られます。残された唯一のことは、許可されている場合のタイプ間の変換です。
これを実現するために、各式を戻り値の型でパラメーター化し、対応する基本クラスから継承します。次に、evaluate
で関数の場合、評価結果をその関数の戻り値に変換します。
たとえば、これは加算式です。
template class add_expression: public expression { ... R evaluate(runtime_context& context) const override{ return convert( _expr1->evaluate(context) + _expr2->evaluate(context) ); } ... };
「変換」関数を作成するには、いくつかのインフラストラクチャが必要です。
template struct is_boxed { static const bool value = false; }; template struct is_boxed, T> { static const bool value = true; }; string convert_to_string(number n) { std::string str if (n == int(n)) { str = std::to_string(int(n)); } else { str = std::to_string(n); } return std::make_shared(std::move(str)); } string convert_to_string(const lnumber& v) { return convert_to_string(v->value); }
構造is_boxed
は、内部定数value
を持つ型特性であり、最初のパラメーターがvariable_impl
への共有ポインターである場合にのみtrueと評価されます。 2番目のタイプでパラメーター化されます。
convert
の実装関数は古いバージョンのC ++でも可能ですが、C ++ 17には、コンパイル時の条件を評価するif constexpr
という非常に便利なステートメントがあります。 false
と評価された場合、コンパイル時エラーが発生したとしても、ブロックは完全に削除されます。それ以外の場合は、else
が削除されますブロック。
template auto convert(From&& from) { if constexpr(std::is_convertible::value) { return std::forward(from); } else if constexpr(is_boxed::value) { return unbox(std::forward(from)); } else if constexpr(std::is_same::value) { return convert_to_string(from); } else { static_assert(std::is_void::value); } }
この関数を読んでみてください:
variable_impl
ポインターアップキャスト用です)。私の意見では、これはSFINAEに基づく古い構文よりもはるかに読みやすくなっています。
式の種類の概要を簡単に説明し、技術的な詳細は省略して、適度に簡潔にします。
式ツリーには、次の3種類のリーフ式があります。
template class global_variable_expression: public expression { private: int _idx; public: global_variable_expression(int idx) : _idx(idx) { } R evaluate(runtime_context& context) const override { return convert( context.global(_idx) ->template static_pointer_downcast() ); } };
戻り値の型とは別に、変数の型でパラメーター化されます。ローカル変数も同様に扱われ、これは定数のクラスです。
template class constant_expression: public expression { private: T _c; public: constant_expression(T c) : _c(std::move(c)) { } R evaluate(runtime_context& context) const override { return convert(_c); } };
この場合、コンストラクターで定数をすぐに変換します。
これは、ほとんどの式の基本クラスとして使用されます。
template class generic_expression: public expression { private: std::tuple _exprs; template R evaluate_tuple( runtime_context& context, const Exprs&... exprs ) const { return convert(O()( std::move(exprs->evaluate(context))...) ); } public: generic_expression(typename expression::ptr... exprs) : _exprs(std::move(exprs)...) { } R evaluate(runtime_context& context) const override { return std::apply( [&](const auto&... exprs){ return this->evaluate_tuple(context, exprs...); }, _exprs ); } };
最初の引数は、インスタンス化されて評価のために呼び出されるファンクタータイプです。残りの型は、子式の戻り値の型です。
ボイラープレートコードを減らすために、次の3つのマクロを定義します。
#define UNARY_EXPRESSION(name, code) struct name##_op { template auto operator()(T1 t1) { code; } }; template using name##_expression = generic_expression; #define BINARY_EXPRESSION(name, code) struct name##_op { template auto operator()(T1 t1, T2 t2) { code; } }; template using name##_expression = generic_expression; #define TERNARY_EXPRESSION(name, code) struct name##_op { template auto operator()(T1 t1, T2 t2, T3 t3) { code; } }; template using name##_expression = generic_expression;
operator()
に注意してくださいはテンプレートとして定義されていますが、通常はそうである必要はありません。マクロ引数として引数タイプを指定するよりも、すべての式を同じ方法で定義する方が簡単です。
これで、式の大部分を定義できます。たとえば、これは/=
の定義です。
BINARY_EXPRESSION(div_assign, t1->value /= t2; return t1; );
これらのマクロを使用して、ほとんどすべての式を定義できます。例外は、引数の評価の順序を定義した演算子(論理&&
および||
、3値(?
)およびコンマ(,
)演算子)、配列インデックス、関数です。を呼び出し、param_expression
を呼び出します。これは、パラメーターを複製して、値で関数に渡します。
これらの実装に複雑なことは何もありません。関数呼び出しの実装は最も複雑なので、ここで説明します。
template class call_expression: public expression{ private: expression::ptr _fexpr; std::vector _exprs; public: call_expression( expression::ptr fexpr, std::vector exprs ): _fexpr(std::move(fexpr)), _exprs(std::move(exprs)) { } R evaluate(runtime_context& context) const override { std::vector params; params.reserve(_exprs.size()); for (size_t i = 0; i evaluate(context)); } function f = _fexpr->evaluate(context); for (size_t i = params.size(); i > 0; --i) { context.push(std::move(params[i-1])); } context.call(); f(context); if constexpr (std::is_same::value) { context.end_function(_exprs.size()); } else { return convert( context.end_function( _exprs.size() )->template static_pointer_downcast() ); } } };
runtime_context
を準備します評価されたすべての引数をスタックにプッシュし、call
を呼び出します。関数。次に、評価された最初の引数(関数自体)を呼び出し、end_function
の戻り値を返します。方法。 if constexpr
の使用法を確認できますここでも構文。これにより、void
を返す関数のクラス全体の特殊化を作成する必要がなくなります。
これで、実行時に使用できる式に関連するすべてのものができました。残っているのは、解析された式ツリー(前のブログ投稿で説明)から式ツリーへの変換だけです。
混乱を避けるために、言語開発サイクルのさまざまなフェーズに名前を付けましょう。
ビジネスの意思決定では、マネージャーは通常、次の2つの基本的な要素を調べます。
式ビルダーの擬似コードは次のとおりです。
function build_expression(nodeptr n, compiler_context context) { if (n is constant) { return constant_expression(n.value); } else if (n is identifier) { id_info info = context.find(n.value); if (context.is_global(info)) { return global_variable_expression(info.index); } else { return local_variable_expression(info.index); } } else { //operation switch (n->value) { case preinc: return preinc_expression( build_expression(n->child[0]) ); ... case add: return add_expression( build_expression(n->child[0]), build_expression(n->child[1]) ); ... case call: return call_expression( n->child[0], //function n->child[1], //arg0 ... n->child[k+1], //argk ); } } }
すべての操作を処理する必要があることを除けば、これは単純なアルゴリズムのように思えます。
それがうまくいけば、それは素晴らしいことですが、そうではありません。手始めに、関数の戻り値の型を指定する必要がありますが、戻り値の型はアクセスしているノードの型に依存するため、ここでは明らかに固定されていません。ノードタイプはコンパイル時に認識されますが、戻り値の型はメタコンパイル時に認識される必要があります。
の中に 前の投稿 、動的型チェックを行う言語の利点がわからないと述べました。このような言語では、上記の擬似コードはほぼ文字通りに実装できます。今、私は動的型言語の利点をよく知っています。最高のインスタントカルマ。
幸いなことに、最上位の式のタイプはわかっています。コンパイルのコンテキストによって異なりますが、式ツリーを解析せずにタイプを知っています。たとえば、forループがある場合:
for (expression1; expression2; expression3) ...
1番目と3番目の式にはvoid
があります評価結果には何もしないため、戻り値の型。ただし、2番目の式のタイプはnumber
です。ループを停止するかどうかを決定するために、ゼロと比較しているためです。
ノード操作に関連する式のタイプがわかっている場合、通常はその子式のタイプを決定します。
たとえば、式(expression1) += (expression2)
の場合タイプはlnumber
です。つまり、expression1
です。そのタイプもあり、expression2
タイプはnumber
です。
ただし、式(expression1) <(expression2)
常にタイプnumber
がありますが、子式のタイプはnumber
です。または、string
と入力します。この式の場合、両方のノードが数値であるかどうかを確認します。もしそうなら、私たちはexpression1
を構築しますおよびexpression2
expression
として。それ以外の場合は、タイプexpression
になります。
私たちが考慮し、対処しなければならない別の問題があります。
タイプnumber
の式を作成する必要がある場合を想像してみてください。次に、連結演算子に遭遇した場合、有効なものを返すことはできません。式ツリー(前の部分)を作成したときに型をすでに確認しているため、発生しないことはわかっていますが、戻り値の型でパラメーター化されたテンプレート関数を作成できないことを意味します。その戻り値の型について。
1つのアプローチでは、if constexpr
を使用して戻り値の型で関数を分割しますが、同じ操作が複数のブランチに存在する場合、そのコードを繰り返す必要があるため、非効率的です。その場合、別々の関数を書くことができます。
実装されたソリューションは、ノードタイプに基づいて関数を分割します。各ブランチで、そのブランチタイプが関数の戻り値のタイプに変換可能かどうかを確認します。そうでない場合は、発生しないはずなのでコンパイラエラーをスローしますが、コードが複雑すぎて、このような強力な主張はできません。エラーが発生した可能性があります。
兌換性をチェックするために、次の自明の型特性構造を使用しています。
template struct is_convertible std::is_same::value ) ); ;
その分割後、コードはほとんど簡単です。元の式タイプからビルドする式タイプにセマンティックにキャストでき、メタコンパイル時にエラーは発生しません。
ただし、定型コードはたくさんあるので、それを減らすためにマクロに大きく依存しました。
vuejsサーバー側レンダリング
template class expression_builder{ private: using expression_ptr = typename expression::ptr; static expression_ptr build_void_expression( const node_ptr& np, compiler_context& context ); static expression_ptr build_number_expression( const node_ptr& np, compiler_context& context ); static expression_ptr build_lnumber_expression( const node_ptr& np, compiler_context& context ); static expression_ptr build_string_expression( const node_ptr& np, compiler_context& context ); static expression_ptr build_lstring_expression( const node_ptr& np, compiler_context& context ); static expression_ptr build_array_expression( const node_ptr& np, compiler_context& context ); static expression_ptr build_larray_expression( const node_ptr& np, compiler_context& context ); static expression_ptr build_function_expression( const node_ptr& np, compiler_context& context ); static expression_ptr build_lfunction_expression( const node_ptr& np, compiler_context& context ); public: static expression_ptr build_expression( const node_ptr& np, compiler_context& context ) { return std::visit(overloaded{ [&](simple_type st){ switch (st) { case simple_type::number: if (np->is_lvalue()) { RETURN_EXPRESSION_OF_TYPE(lnumber); } else { RETURN_EXPRESSION_OF_TYPE(number); } case simple_type::string: if (np->is_lvalue()) { RETURN_EXPRESSION_OF_TYPE(lstring); } else { RETURN_EXPRESSION_OF_TYPE(string); } case simple_type::nothing: RETURN_EXPRESSION_OF_TYPE(void); } }, [&](const function_type& ft) { if (np->is_lvalue()) { RETURN_EXPRESSION_OF_TYPE(lfunction); } else { RETURN_EXPRESSION_OF_TYPE(function); } }, [&](const array_type& at) { if (np->is_lvalue()) { RETURN_EXPRESSION_OF_TYPE(larray); } else { RETURN_EXPRESSION_OF_TYPE(array); } } }, *np->get_type_id()); } };
関数build_expression
ここで唯一のパブリック関数です。関数std::visit
を呼び出しますノードタイプ。この関数は、渡されたファンクターをvariant
に適用し、プロセスでデカップリングします。あなたはそれとファンクターについてもっと読むことができますoverloaded
ここに 。
マクロRETURN_EXPRESSION_OF_TYPE
式を作成するためにプライベート関数を呼び出し、変換が不可能な場合は例外をスローします。
#define RETURN_EXPRESSION_OF_TYPE(T) if constexpr(is_convertible::value) { return build_##T##_expression(np, context); } else { throw expression_builder_error(); return expression_ptr(); }
変換が不可能な場合、コンパイラは関数の戻り値の型を認識できないため、elseブランチに空のポインタを返す必要があります。それ以外の場合、std::visit
オーバーロードされたすべての関数が同じ戻り値の型を持つ必要があります。
たとえば、string
で式を作成する関数があります。戻り値の型として:
static expression_ptr build_string_expression( const node_ptr& np, compiler_context& context ) { if (std::holds_alternative(np->get_value())) { return std::make_unique( std::make_shared( std::get(np->get_value()) ) ); } CHECK_IDENTIFIER(lstring); switch (std::get(np->get_value())) { CHECK_BINARY_OPERATION(concat, string, string); CHECK_BINARY_OPERATION(comma, void, string); CHECK_TERNARY_OPERATION(ternary, number, string, string); CHECK_INDEX_OPERATION(lstring); CHECK_CALL_OPERATION(lstring); default: throw expression_builder_error(); } }
ノードが文字列定数を保持しているかどうかをチェックし、constant_expression
を構築しますだとしたら。
次に、ノードが識別子を保持しているかどうかを確認し、その場合はlstringタイプのグローバル変数式またはローカル変数式を返します。定数変数を実装すると、識別子を保持できます。それ以外の場合は、ノードがノード操作を保持していると想定し、string
を返すことができるすべての操作を試行します。
CHECK_IDENTIFIER
の実装は次のとおりです。およびCHECK_BINARY_OPERATION
マクロ:
#define CHECK_IDENTIFIER(T1) if (std::holds_alternative(np->get_value())) { const identifier& id = std::get(np->get_value()); const identifier_info* info = context.find(id.name); if (info->is_global()) { return std::make_unique< global_variable_expression>(info->index()); } else { return std::make_unique< local_variable_expression>(info->index()); } }
#define CHECK_BINARY_OPERATION(name, T1, T2) case node_operation::name: return expression_ptr( std::make_unique ( expression_builder::build_expression( np->get_children()[0], context ), expression_builder::build_expression( np->get_children()[1], context ) ) );
CHECK_IDENTIFIER
マクロは参照する必要がありますcompiler_context
適切なインデックスを使用してグローバル変数またはローカル変数の式を作成します。 compiler_context
の唯一の使用法ですこの構造で。
あなたはそれを見ることができますCHECK_BINARY_OPERATION
build_expression
を再帰的に呼び出します子ノード用。
で 私のGitHubページ 、完全なソースコードを取得してコンパイルし、式を入力して、評価された変数の結果を確認できます。
人間の創造性のあらゆる分野で、ある意味で自分たちの製品が生きていることに作者が気付く瞬間があると思います。プログラミング言語の構築において、それは言語が「呼吸する」ことを見ることができる瞬間です。
このシリーズの次の最後のパートでは、残りの最小限の言語機能セットを実装して、ライブで実行されることを確認します。
マクロは、入力を出力または特定のアクションにマップする方法を説明するルールです。これは、いくつかの一般的な作業を自動化するために使用できます。
C ++のマクロは、入力パラメーターがC ++コードに変換される方法を説明する関数のようなマッピングと呼ばれます。
チューリングマシンは、現代のコンピューターが実行できるすべてのことを実行できる抽象マシンです。プログラミング言語は、チューリングマシンをエミュレートできる場合、チューリング完全です。
GOTOステートメントは、通常のプログラムフローを突然中断し、コードを読みにくくするため、不良と見なされます。
GOTOステートメントによってコードが読みやすくなる、まれな状況がいくつかあります。たとえば、2つ以上のネストされたループからの離脱などです。