コウノトリ、パート4:ステートメントの実装とまとめ

公開: 2022-03-11

C ++を使用して軽量プログラミング言語を作成するという私たちの探求では、3週間前にトークナイザーを作成することから始め、次の2週間で式の評価を実装しました。

今度は、成熟したプログラミング言語ほど強力ではないが、非常に小さなフットプリントを含むすべての必要な機能を備えた完全なプログラミング言語をまとめて提供するときです。

新しい会社のWebサイトに、頻繁に尋ねられる質問ではなく、聞きたい質問に答えるFAQセクションがあるのはおかしいと思います。 ここでも同じことをします。 私の仕事をフォローしている人は、なぜStorkがバイトコードまたは少なくともいくつかの中間言語にコンパイルされないのかとよく尋ねます。

なぜコウノトリはバイトコードにコンパイルされないのですか?

私はこの質問に喜んで答えます。 私の目標は、C++と簡単に統合できる小さなフットプリントのスクリプト言語を開発することでした。 「小さなフットプリント」の厳密な定義はありませんが、それほど強力ではないデバイスへの移植を可能にするのに十分小さいコンパイラであり、実行時にあまり多くのメモリを消費しないコンパイラを想像します。

C++コウノトリ

タイムクリティカルなタスクがある場合はC++でコーディングすると思うので、速度には重点を置きませんでしたが、何らかの拡張性が必要な場合は、Storkのような言語が役立つ可能性があります。

同様のタスクを実行できる、より優れた言語が他にないとは言いません(たとえば、Lua)。 それらが存在しなかったとしたら、それは本当に悲劇的なことであり、私は単にこの言語のユースケースのアイデアをあなたに与えているだけです。

これはC++に組み込まれるため、同様の目的を果たすエコシステム全体を作成する代わりに、C++の既存の機能を使用すると便利です。 それだけでなく、このアプローチの方が面白いと思います。

いつものように、完全なソースコードは私のGitHubページにあります。 それでは、進捗状況を詳しく見ていきましょう。

変更点

この部分まで、コウノトリは部分的に完全な製品だったので、私はその欠点と欠陥のすべてを見ることができませんでした。 ただし、より完全な形になったので、前の部分で紹介した次のことを変更しました。

  • 関数はもはや変数ではありません。 現在、 compiler_contextに別のfunction_lookupがあります。 混乱を避けるために、 function_param_lookupparam_lookupに名前が変更されました。
  • 関数の呼び出し方法を変更しました。 runtime_contextには、引数のstd::vectorを取り、古い戻り値インデックスを格納し、引数をスタックにプッシュし、戻り値インデックスを変更し、関数を呼び出し、スタックから引数をポップし、古い戻り値インデックスを復元するcallメソッドがあります。結果を返します。 そうすれば、C ++スタックがその目的を果たすため、以前のように戻り値インデックスのスタックを保持する必要はありません。
  • compiler_contextコンテキストに追加されたRAIIクラスは、そのメンバー関数のscopefunctionの呼び出しによって返されます。 これらの各オブジェクトは、コンストラクターでそれぞれ新しいlocal_identifier_lookupparam_identifier_lookupを作成し、デストラクタで古い状態を復元します。
  • runtime_contextに追加されたRAIIクラスで、メンバー関数get_scopeによって返されます。 この関数は、スタックサイズをコンストラクターに格納し、デストラクタに復元します。
  • 一般的に、 constキーワードと定数オブジェクトを削除しました。 それらは便利かもしれませんが、絶対に必要というわけではありません。
  • 現在はまったく必要ないため、 varキーワードは削除されました。
  • 実行時に配列サイズをチェックするsizeofキーワードを追加しました。 C ++ sizeofはコンパイル時に実行されるため、一部のC ++プログラマーは名前の選択を冒涜的に感じるかもしれませんが、一般的な変数名(たとえば、 size )との衝突を避けるためにそのキーワードを選択しました。
  • 何かを明示的にstringに変換するtostringキーワードを追加しました。 関数のオーバーロードを許可しないため、関数にすることはできません。
  • あまり面白くないさまざまな変更。

構文

Cとそれに関連するプログラミング言語に非常によく似た構文を使用しているので、明確ではないかもしれない詳細だけを説明します。

変数型の宣言は次のとおりです。

  • void 、関数の戻り型にのみ使用されます
  • number
  • string
  • T[]は、タイプTの要素を保持するものの配列です。
  • R(P1,...,Pn)は、タイプRを返し、タイプP1からPnの引数を受け取る関数です。 これらの各タイプは、参照によって渡される場合、接頭辞&を付けることができます。

関数宣言は次のとおりです。 [public] function R name(P1 p1, … Pn pn)

したがって、接頭辞としてfunctionを付ける必要があります。 プレフィックスがpublicの場合は、C++から呼び出すことができます。 関数が値を返さない場合は、戻り型のデフォルト値に評価されます。

最初の式で宣言を使用for -loopを使用できます。 また、C ++ 17のように、初期化式を使用しifステートメントとswitchステートメントを使用することもできます。 ifステートメントはifブロックで始まり、その後に0個以上のelifブロックが続き、オプションでelseの1ブロックが続きます。 変数がifステートメントの初期化式で宣言されている場合、それらの各ブロックに表示されます。

複数のネストされたループからブレークできるbreakステートメントの後にオプションの番号を許可します。 したがって、次のコードを使用できます。

 for (number i = 0; i < 100; ++i) { for(number j = 0; j < 100; ++j) { if (rnd(100) == 0) { break 2; } } }

また、両方のループから抜け出します。 その数はコンパイル時に検証されます。 それはどれくらいクールですか?

コンパイラ

この部分には多くの機能が追加されていますが、詳細になりすぎると、私を支えている最も執拗な読者でさえも失うことになるでしょう。 したがって、私は意図的に物語の非常に大きな部分の1つである編集をスキップします。

これは、このブログシリーズの第1部と第2部ですでに説明したためです。 私は表現に焦点を合わせていましたが、他のものをコンパイルすることはそれほど違いはありません。

ただし、1つの例を示します。 このコードwhileステートメントをコンパイルします。

 statement_ptr compile_while_statement( compiler_context& ctx, tokens_iterator& it, possible_flow pf ) { parse_token_value(ctx, it, reserved_token::kw_while); parse_token_value(ctx, it, reserved_token::open_round); expression<number>::ptr expr = build_number_expression(ctx, it); parse_token_value(ctx, it, reserved_token::close_round); block_statement_ptr block = compile_block_statement(ctx, it, pf); return create_while_statement(std::move(expr), std::move(block)); }

ご覧のとおり、複雑ではありません。 、then ( 、次に数値式を作成し(ブール値はありません)、次に)を解析while )

その後、 {}の内部にあるかどうかに関係なく(はい、単一ステートメントのブロックを許可しました)ブロックステートメントをコンパイルし、最後にwhileステートメントを作成します。

あなたはすでに最初の2つの関数の引数に精通しています。 3番目のpossible_flowは、解析しているコンテキストで許可されているフロー変更コマンド( continuebreakreturn )を示しています。 コンパイルステートメントがいくつかのcompilerクラスのメンバー関数である場合、その情報をオブジェクトに保持できますが、私は巨大なクラスの大ファンではなく、コンパイラは間違いなくそのようなクラスの1つです。 余分な引数、特に薄い引数を渡しても、誰も傷つけることはありません。おそらくいつか、コードを並列化できるようになるでしょう。

ここで説明したい編集のもう1つの興味深い側面があります。

2つの関数が相互に呼び出しているシナリオをサポートする場合は、前方宣言を許可するか、2つのコンパイルフェーズを使用することで、C-wayで実行できます。

私は2番目のアプローチを選びました。 関数定義が見つかると、その型と名前を解析して、 incomplete_functionという名前のオブジェクトにします。 次に、最初のカーリーブレースを閉じるまで、カーリーブレースのネストレベルを数えるだけで、解釈せずにその本体をスキップします。 プロセスでトークンを収集し、それらをincomplete_functionに保持し、関数識別子をcompiler_contextに追加します。

ファイル全体を渡したら、各関数を完全にコンパイルして、実行時に呼び出すことができるようにします。 このようにして、各関数はファイル内の他の関数を呼び出したり、グローバル変数にアクセスしたりできます。

グローバル変数は、同じ関数を呼び出すことで初期化できます。これにより、これらの関数が初期化されていない変数にアクセスするとすぐに、古い「鶏が先か卵が先か」の問題が発生します。

それが発生した場合、 runtime_exceptionをスローすることで問題が解決されます。これは、私が優れているからです。 フランキー、アクセス違反は、そのようなコードを書いた場合の罰として得ることができる最小のものです。

グローバルスコープ

グローバルスコープに表示できるエンティティには、次の2種類があります。

  • グローバル変数
  • 関数

各グローバル変数は、正しい型を返す式で初期化できます。 初期化子は、グローバル変数ごとに作成されます。

各初期化子はlvalueを返すため、グローバル変数のコンストラクターとして機能します。 グローバル変数に式が指定されていない場合、デフォルトの初期化子が作成されます。

これはruntime_contextinitializeメンバー関数です:

 void runtime_context::initialize() { _globals.clear(); for (const auto& initializer : _initializers) { _globals.emplace_back(initializer->evaluate(*this)); } }

コンストラクターから呼び出されます。 明示的に呼び出すことができるように、グローバル変数コンテナをクリアして、 runtime_context状態をリセットします。

前述したように、初期化されていないグローバル変数にアクセスするかどうかを確認する必要があります。 したがって、これはグローバル変数アクセサーです。

 variable_ptr& runtime_context::global(int idx) { runtime_assertion( idx < _globals.size(), "Uninitialized global variable access" ); return _globals[idx]; }

最初の引数がfalseと評価された場合、 runtime_assertionは対応するメッセージとともにruntime_errorをスローします。

各関数は、単一のステートメントをキャプチャするラムダとして実装され、関数が受け取るruntime_contextで評価されます。

機能範囲

whileステートメントのコンパイルからわかるように、コンパイラーは、関数全体のブロックを表すblockステートメントから始めて再帰的に呼び出されます。

すべてのステートメントの抽象基本クラスは次のとおりです。

 class statement { statement(const statement&) = delete; void operator=(const statement&) = delete; protected: statement() = default; public: virtual flow execute(runtime_context& context) = 0; virtual ~statement() = default; };

デフォルトの関数以外の唯一の関数はexecuteです。これは、 runtime_contextでステートメントロジックを実行し、プログラムロジックが次に進む場所を決定するflowを返します。

 enum struct flow_type{ f_normal, f_break, f_continue, f_return, }; class flow { private: flow_type _type; int _break_level; flow(flow_type type, int break_level); public: flow_type type() const; int break_level() const; static flow normal_flow(); static flow break_flow(int break_level); static flow continue_flow(); static flow return_flow(); flow consume_break(); };

静的クリエーター関数は自明であり、ゼロ以外のbreak_levelflow_type::f_breakとは異なるタイプの非論理的なflowを防ぐために作成しました。

これで、 consume_breakは、ブレークレベルが1つ少ないブレークフローを作成します。ブレークレベルがゼロに達した場合は、通常のフローを作成します。

次に、すべてのステートメントタイプを確認します。

 class simple_statement: public statement { private: expression<void>::ptr _expr; public: simple_statement(expression<void>::ptr expr): _expr(std::move(expr)) { } flow execute(runtime_context& context) override { _expr->evaluate(context); return flow::normal_flow(); } };

ここで、 simple_statementは、式から作成されるステートメントです。 すべての式はvoidを返す式としてコンパイルできるため、そこからsimple_statementを作成できます。 breakcontinuereturnも式の一部にすることはできないため、 simple_statementflow::normal_flow()返します。

 class block_statement: public statement { private: std::vector<statement_ptr> _statements; public: block_statement(std::vector<statement_ptr> statements): _statements(std::move(statements)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const statement_ptr& statement : _statements) { if ( flow f = statement->execute(context); f.type() != flow_type::f_normal ){ return f; } } return flow::normal_flow(); } };

block_statementは、ステートメントのstd::vectorを保持します。 それらを1つずつ実行します。 それぞれが非通常のフローを返す場合、そのフローをすぐに返します。 RAIIスコープオブジェクトを使用して、ローカルスコープ変数の宣言を許可します。

 class local_declaration_statement: public statement { private: std::vector<expression<lvalue>::ptr> _decls; public: local_declaration_statement(std::vector<expression<lvalue>::ptr> decls): _decls(std::move(decls)) { } flow execute(runtime_context& context) override { for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return flow::normal_flow(); } };

local_declaration_statementは、ローカル変数を作成する式を評価し、新しいローカル変数をスタックにプッシュします。

 class break_statement: public statement { private: int _break_level; public: break_statement(int break_level): _break_level(break_level) { } flow execute(runtime_context&) override { return flow::break_flow(_break_level); } };

break_statementには、コンパイル時に評価されるブレークレベルがあります。 そのブレークレベルに対応するフローを返すだけです。

 class continue_statement: public statement { public: continue_statement() = default; flow execute(runtime_context&) override { return flow::continue_flow(); } };

continue_statementは、 flow::continue_flow()を返すだけです。

 class return_statement: public statement { private: expression<lvalue>::ptr _expr; public: return_statement(expression<lvalue>::ptr expr) : _expr(std::move(expr)) { } flow execute(runtime_context& context) override { context.retval() = _expr->evaluate(context); return flow::return_flow(); } }; class return_void_statement: public statement { public: return_void_statement() = default; flow execute(runtime_context&) override { return flow::return_flow(); } };

return_statementreturn_void_statementはどちらもflow::return_flow()を返します。 唯一の違いは、前者には、戻る前に戻り値に評価される式があることです。

 class if_statement: public statement { private: std::vector<expression<number>::ptr> _exprs; std::vector<statement_ptr> _statements; public: if_statement( std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements ): _exprs(std::move(exprs)), _statements(std::move(statements)) { } flow execute(runtime_context& context) override { for (size_t i = 0; i < _exprs.size(); ++i) { if (_exprs[i]->evaluate(context)) { return _statements[i]->execute(context); } } return _statements.back()->execute(context); } }; class if_declare_statement: public if_statement { private: std::vector<expression<lvalue>::ptr> _decls; public: if_declare_statement( std::vector<expression<lvalue>::ptr> decls, std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements ): if_statement(std::move(exprs), std::move(statements)), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return if_statement::execute(context); } };

if_statementは、1つのif -block、0個以上のelif -block、および1つのelse -block(空の場合もあります)に対して作成され、1つの式が1と評価されるまで、各式を評価します。 次に、そのブロックを実行し、実行結果を返します。 1と評価される式がない場合は、最後の( else )ブロックの実行を返します。

if_declare_statementは、if句の最初の部分として宣言を持つステートメントです。 宣言されたすべての変数をスタックにプッシュしてから、その基本クラス( if_statement )を実行します。

 class switch_statement: public statement { private: expression<number>::ptr _expr; std::vector<statement_ptr> _statements; std::unordered_map<number, size_t> _cases; size_t _dflt; public: switch_statement( expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt ): _expr(std::move(expr)), _statements(std::move(statements)), _cases(std::move(cases)), _dflt(dflt) { } flow execute(runtime_context& context) override { auto it = _cases.find(_expr->evaluate(context)); for ( size_t idx = (it == _cases.end() ? _dflt : it->second); idx < _statements.size(); ++idx ) { switch (flow f = _statements[idx]->execute(context); f.type()) { case flow_type::f_normal: break; case flow_type::f_break: return f.consume_break(); default: return f; } } return flow::normal_flow(); } }; class switch_declare_statement: public switch_statement { private: std::vector<expression<lvalue>::ptr> _decls; public: switch_declare_statement( std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt ): _decls(std::move(decls)), switch_statement(std::move(expr), std::move(statements), std::move(cases), dflt) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return switch_statement::execute(context); } };

switch_statementは、ステートメントを1つずつ実行しますが、最初に、式の評価から取得した適切なインデックスにジャンプします。 そのステートメントのいずれかが非通常のフローを返す場合、そのフローはすぐに返されます。 flow_type::f_breakがある場合、最初に1つのブレークを消費します。

switch_declare_statementは、ヘッダーでの宣言を許可します。 それらのどれも、本文での宣言を許可していません。

 class while_statement: public statement { private: expression<number>::ptr _expr; statement_ptr _statement; public: while_statement(expression<number>::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { while (_expr->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } };
 class do_statement: public statement { private: expression<number>::ptr _expr; statement_ptr _statement; public: do_statement(expression<number>::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { do { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } while (_expr->evaluate(context)); return flow::normal_flow(); } };

while_statementdo_while_statementはどちらも、式が1と評価されている間、bodyステートメントを実行します。 実行がflow_type::f_breakを返す場合、それらはそれを消費して戻ります。 flow_type::f_returnを返す場合は、それを返します。 通常の実行または続行の場合、それらは何もしません。

continueしても効果がないように見える場合があります。 ただし、内部ステートメントはその影響を受けました。 たとえば、 block_statementの場合、最後まで評価されませんでした。

while_statementがC++ whileで実装され、 do-statementがC++ do-whileで実装されているのは素晴らしいことです。

 class for_statement_base: public statement { private: expression<number>::ptr _expr2; expression<void>::ptr _expr3; statement_ptr _statement; public: for_statement_base( expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): _expr2(std::move(expr2)), _expr3(std::move(expr3)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { for (; _expr2->evaluate(context); _expr3->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } }; class for_statement: public for_statement_base { private: expression<void>::ptr _expr1; public: for_statement( expression<void>::ptr expr1, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _expr1(std::move(expr1)) { } flow execute(runtime_context& context) override { _expr1->evaluate(context); return for_statement_base::execute(context); } }; class for_declare_statement: public for_statement_base { private: std::vector<expression<lvalue>::ptr> _decls; expression<number>::ptr _expr2; expression<void>::ptr _expr3; statement_ptr _statement; public: for_declare_statement( std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return for_statement_base::execute(context); } };

for_statementおよびfor_statement_declareは、 while_statementおよびdo_statementと同様に実装されます。 これらは、ほとんどのロジックを実行するfor_statement_baseクラスから継承されます。 for_statement_declareは、 forループの最初の部分が変数宣言である場合に作成されます。

C ++コウノトリ:ステートメントの実装

これらはすべて私たちが持っているステートメントクラスです。 それらは私たちの機能の構成要素です。 runtime_contextが作成されると、それらの関数が保持されます。 関数がキーワードpublicで宣言されている場合、名前で呼び出すことができます。

これで、Storkのコア機能は終わりです。 私が説明する他のすべては、私たちの言語をより有用にするために私が追加した後付けです。

タプル

配列は、単一タイプの要素のみを含むことができるため、同種のコンテナーです。 異種のコンテナが必要な場合は、構造がすぐに思い浮かびます。

ただし、より些細な異種コンテナーがあります:タプル。 タプルはさまざまなタイプの要素を保持できますが、それらのタイプはコンパイル時に認識されている必要があります。 これは、Storkでのタプル宣言の例です。

 [number, string] t = {22321, "Siveric"};

これにより、 numberstringのペアが宣言され、初期化されます。

初期化リストを使用して、配列を初期化することもできます。 初期化リストの式の型が変数の型と一致しない場合、コンパイラエラーが発生します。

配列はvariable_ptrのコンテナーとして実装されているため、タプルのランタイム実装を無料で入手できます。 含まれている変数の正しいタイプを保証するのはコンパイル時です。

モジュール

Storkユーザーから実装の詳細を隠し、よりユーザーフレンドリーな方法で言語を提示するとよいでしょう。

これは私たちがそれを達成するのに役立つクラスです。 私は実装の詳細なしでそれを提示します:

 class module { ... public: template<typename R, typename... Args> void add_external_function(const char* name, std::function<R(Args...)> f); template<typename R, typename... Args> auto create_public_function_caller(std::string name); void load(const char* path); bool try_load(const char* path, std::ostream* err = nullptr) noexcept; void reset_globals(); ... };

関数loadおよびtry_loadは、指定されたパスからStorkスクリプトをロードしてコンパイルします。 まず、そのうちの1つはstork::errorをスローできますが、2つ目はそれをキャッチして、提供されている場合は出力に出力します。

関数reset_globalsは、グローバル変数を再初期化します。

関数add_external_functionsおよびcreate_public_function_callerは、コンパイルの前に呼び出す必要があります。 1つ目は、Storkから呼び出すことができるC++関数を追加します。 2つ目は、C++からStork関数を呼び出すために使用できる呼び出し可能オブジェクトを作成します。 Storkスクリプトのコンパイル中にパブリック関数の型がR(Args…)と一致しない場合、コンパイル時エラーが発生します。

Storkモジュールに追加できるいくつかの標準関数を追加しました。

 void add_math_functions(module& m); void add_string_functions(module& m); void add_trace_functions(module& m); void add_standard_functions(module& m);

Storkスクリプトの例を次に示します。

 function void swap(number& x, number& y) { number tmp = x; x = y; y = tmp; } function void quicksort( number[]& arr, number begin, number end, number(number, number) comp ) { if (end - begin < 2) return; number pivot = arr[end-1]; number i = begin; for (number j = begin; j < end-1; ++j) if (comp(arr[j], pivot)) swap(&arr[i++], &arr[j]); swap (&arr[i], &arr[end-1]); quicksort(&arr, begin, i, comp); quicksort(&arr, i+1, end, comp); } function void sort(number[]& arr, number(number, number) comp) { quicksort(&arr, 0, sizeof(arr), comp); } function number less(number x, number y) { return x < y; } public function void main() { number[] arr; for (number i = 0; i < 100; ++i) { arr[sizeof(arr)] = rnd(100); } trace(tostring(arr)); sort(&arr, less); trace(tostring(arr)); sort(&arr, greater); trace(tostring(arr)); }

これがC++の部分です:

 #include <iostream> #include "module.hpp" #include "standard_functions.hpp" int main() { std::string path = __FILE__; path = path.substr(0, path.find_last_of("/\\") + 1) + "test.stk"; using namespace stork; module m; add_standard_functions(m); m.add_external_function( "greater", std::function<number(number, number)>([](number x, number y){ return x > y; } )); auto s_main = m.create_public_function_caller<void>("main"); if (m.try_load(path.c_str(), &std::cerr)) { s_main(); } return 0; }

コンパイル前に標準関数がモジュールに追加され、関数traceおよびrndがStorkスクリプトから使用されます。 関数greaterもショーケースとして追加されています。

スクリプトは、「main.cpp」と同じフォルダーにあるファイル「test.stk」から( __FILE__プリプロセッサー定義を使用して)ロードされ、関数mainが呼び出されます。

スクリプトでは、ランダム配列を生成し、コンパレータlessを使用して昇順でソートし、次にC++で記述されたコンパレータgreaterを使用して降順でソートします。

このコードは、C(またはCから派生したプログラミング言語)に堪能な人なら誰でも完全に読めることがわかります。

次はどうする?

Storkに実装したい機能はたくさんあります。

  • 構造
  • クラスと継承
  • モジュール間呼び出し
  • ラムダ関数
  • 動的に型付けされたオブジェクト

時間とスペースの不足は、それらをまだ実装していない理由の1つです。 暇なときに新しい機能を実装するときに、GitHubページを新しいバージョンで更新しようと思います。

まとめ

新しいプログラミング言語を作成しました!

これは、過去6週間の私の空き時間のかなりの部分を占めていましたが、今ではいくつかのスクリプトを記述して、それらが実行されていることを確認できます。 それは私が過去数日間やっていたことであり、予期せずクラッシュするたびに禿げた頭を引っ掻いた。 時にはそれは小さなバグであり、時には厄介なバグでした。 しかし、他の時には、私がすでに世界と共有していた悪い決断についてだったので、私は恥ずかしい思いをしました。 しかし、毎回、私は修正してコーディングを続けていました。

その過程で、これまで使ったことのないif constexprを学びました。 また、右辺値参照と完全な転送、および日常的に遭遇しないC++17の他の小さな機能についてもよく理解しました。

コードは完全ではありません—私はそのような主張をすることは決してありません—しかしそれは十分に良いです、そしてそれはほとんど良いプログラミング慣行に従います。 そして最も重要なのは、それが機能することです。

新しい言語を最初から開発することを決定することは、平均的な人にとって、あるいは平均的なプログラマーにとってさえ狂気に聞こえるかもしれませんが、それはそれを実行し、あなたがそれを実行できることを自分自身に証明する理由です。 難しいパズルを解くのと同じように、精神的に健康を保つための良い脳の運動です。

退屈な課題は、私たちの日常のプログラミングでは一般的です。なぜなら、それの興味深い側面だけを選択することはできず、時には退屈であっても真剣な仕事をしなければならないからです。 あなたがプロの開発者である場合、あなたの最優先事項はあなたの雇用主に高品質のコードを提供し、テーブルに食べ物を置くことです。 これにより、暇なときにプログラミングを回避できる場合があり、プログラミングの初期の学生時代の熱意を弱める可能性があります。

必要がなければ、その熱意を失わないでください。 すでに完了している場合でも、面白いと思ったら何かに取り組みます。 楽しむために理由を正当化する必要はありません。

そして、それを部分的にでもあなたの専門的な仕事に取り入れることができれば、あなたにとって良いことです! その機会を持っている人は多くありません。

この部分のコードは、GitHubページの専用ブランチでフリーズされます。