Stork,第 4 部分:實現語句和總結

已發表: 2022-03-11

在我們使用 C++ 創建輕量級編程語言的過程中,我們從三週前創建標記器開始,然後在接下來的兩週內實現了表達式評估。

現在,是時候結束並交付一種完整的編程語言了,它不會像成熟的編程語言那麼強大,但將具有所有必要的特性,包括非常小的佔用空間。

我覺得有趣的是,新公司在其網站上的常見問題解答部分不回答經常被問到的問題,而是他們想被問到的問題。 我會在這裡做同樣的事情。 關注我工作的人經常問我為什麼 Stork 不能編譯成一些字節碼或至少是一些中間語言。

為什麼 Stork 不編譯為字節碼?

我很高興回答這個問題。 我的目標是開發一種可以輕鬆與 C++ 集成的小型腳本語言。 我對“佔用空間小”沒有嚴格的定義,但我認為編譯器應該足夠小,可以移植到功能較弱的設備上,並且在運行時不會消耗太多內存。

C++ 鸛

我沒有關注速度,因為我認為如果你有一個時間緊迫的任務,你會用 C++ 編寫代碼,但是如果你需要某種可擴展性,那麼像 Stork 這樣的語言可能會很有用。

我並不是說沒有其他更好的語言可以完成類似的任務(例如,Lua)。 如果它們不存在,那將是真正的悲劇,我只是讓您了解這種語言的用例。

由於它將嵌入到 C++ 中,我發現使用 C++ 的一些現有特性而不是編寫一個用於類似目的的整個生態系統很方便。 不僅如此,我還發現這種方法更有趣。

與往常一樣,您可以在我的 GitHub 頁面上找到完整的源代碼。 現在,讓我們仔細看看我們的進展。

變化

到目前為止,Stork 是一個部分完整的產品,所以我無法看到它的所有缺點和缺陷。 但是,由於它的形狀更完整,我更改了前面部分介紹的以下內容:

  • 函數不再是變量。 現在compiler_context中有一個單獨的function_lookupfunction_param_lookup被重命名為param_lookup以避免混淆。
  • 我改變了調用函數的方式。 runtime_context中有一個call方法,它接受參數的std::vector ,存儲舊的返回值索引,將參數壓入堆棧,更改返回值索引,調用函數,從堆棧中彈出參數,恢復舊的返回值索引,以及返回結果。 這樣,我們就不必像以前那樣保留返回值索引的堆棧,因為 C++ 堆棧服務於這個目的。
  • compiler_context中添加的 RAII 類,由對其成員函數scopefunction的調用返回。 這些對像中的每一個都分別在其構造函數中創建新的local_identifier_lookupparam_identifier_lookup ,並在析構函數中恢復舊狀態。
  • runtime_context中添加的 RAII 類,由成員函數get_scope返回。 該函數將堆棧大小存儲在其構造函數中並在其析構函數中恢復它。
  • 我一般刪除了const關鍵字和常量對象。 它們可能有用,但不是絕對必要的。
  • var關鍵字已刪除,因為目前根本不需要它。
  • 我添加了sizeof關鍵字,它將在運行時檢查數組大小。 也許一些 C++ 程序員會發現名稱選擇是褻瀆神明的,因為 C++ sizeof在編譯時運行,但我選擇該關鍵字是為了避免與一些常見的變量名稱衝突 - 例如size
  • 我添加了tostring關鍵字,它將任何內容顯式轉換為string 。 它不能是函數,因為我們不允許函數重載。
  • 各種不太有趣的變化。

句法

由於我們使用的語法與 C 及其相關編程語言非常相似,因此我將僅提供可能不清楚的細節。

變量類型聲明如下:

  • void ,僅用於函數返回類型
  • number
  • string
  • T[]是一個包含T類型元素的數組
  • R(P1,...,Pn)是一個函數,它返回類型R並接收類型為P1Pn的參數。 如果通過引用傳遞,則每種類型都可以使用&作為前綴。

函數聲明如下: [public] function R name(P1 p1, … Pn pn)

因此,它必須以function為前綴。 如果它以public為前綴,則可以從 C++ 調用它。 如果函數沒有返回值,它將評估為其返回類型的默認值。

我們允許for -loop 在第一個表達式中聲明。 我們還允許帶有初始化表達式的if語句和switch語句,如在 C++17 中。 if語句以if塊開頭,後跟零個或多個elif塊,以及可選的一個else塊。 如果變量是在if語句的初始化表達式中聲明的,那麼它將在每個塊中可見。

我們允許在可以從多個嵌套循環中中斷的break語句之後使用可選數字。 所以你可以有以下代碼:

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

此外,它將從兩個循環中中斷。 該數字在編譯時得到驗證。 多麼酷啊?

編譯器

這部分添加了許多功能,但如果我過於詳細,我可能會失去即使是最堅持不懈的讀者,但仍然與我保持一致。 因此,我會故意跳過故事的一個非常大的部分——編譯。

那是因為我已經在這個博客系列的第一和第二部分中描述了它。 我專注於表達式,但編譯其他任何東西並沒有太大的不同。

不過,我會給你舉一個例子。 此代碼編譯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)); }

如您所見,它遠非複雜。 它解析while ,然後( ,然後它構建一個數字表達式(我們沒有布爾值),然後解析)

之後,它編譯一個可能在{}內的塊語句(是的,我允許單語句塊),並在最後創建一個while語句。

您已經熟悉前兩個函數參數。 第三個, possible_flow ,顯示了我們正在解析的上下文中允許的流更改命令( continuebreakreturn )。 如果編譯語句是某個compiler類的成員函數,我可以將這些信息保留在對像中,但我不是龐大的類的忠實粉絲,編譯器肯定是這樣的一個類。 傳遞一個額外的參數,尤其是一個細小的參數,不會傷害任何人,誰知道呢,也許有一天我們將能夠並行化代碼。

我想在這裡解釋編譯的另一個有趣的方面。

如果我們想支持兩個函數相互調用的場景,我們可以使用 C 方式:允許前向聲明或有兩個編譯階段。

我選擇了第二種方法。 找到函數定義後,我們會將其類型和名稱解析為名為incomplete_function的對象。 然後,我們將跳過它的主體,不做解釋,只計算花括號的嵌套級別,直到我們關閉第一個花括號。 我們將在這個過程中收集令牌,將它們保存在incomplete_function的函數中,並將函數標識符添加到compiler_context中。

一旦我們傳遞了整個文件,我們將完整地編譯每個函數,以便可以在運行時調用它們。 這樣,每個函數都可以調用文件中的任何其他函數,並且可以訪問任何全局變量。

全局變量可以通過調用相同的函數來初始化,一旦這些函數訪問未初始化的變量,我們就會立即陷入舊的“雞和蛋”問題。

如果發生這種情況,可以通過拋出runtime_exception來解決問題——這只是因為我很好。 弗蘭奇,訪問違規是編寫此類代碼的最少懲罰。

全球範圍

有兩種實體可以出現在全局範圍內:

  • 全局變量
  • 職能

每個全局變量都可以使用返回正確類型的表達式進行初始化。 為每個全局變量創建初始化程序。

每個初始值設定項都返回lvalue ,因此它們充當全局變量的構造函數。 如果沒有為全局變量提供表達式,則構造默認初始值設定項。

這是runtime_context中的initialize成員函數:

 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

每個函數都實現為捕獲單個語句的 lambda,然後使用函數接收的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_level和與flow_type::f_break不同的類型的不合邏輯flow

現在, consume_break將創建一個中斷級別少一個的中斷流,或者,如果中斷級別達到零,則創建正常流。

現在,我們將檢查所有語句類型:

 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_statement返回flow::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 。 它一個一個地執行它們。 如果它們中的每一個都返回非正常流,它會立即返回該流。 它使用 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是為一個if塊、零個或多個elif塊和一個else塊(可能為空)創建的,它計算每個表達式,直到一個表達式計算為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一個一個地執行它的語句,但它首先跳轉到它從表達式求值中獲得的適當索引。 如果它的任何語句返回非正常流,它將立即返回該流。 如果它有flow_type::f_break ,它將首先消耗一個中斷。

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時執行它們的主體語句。 如果執行返回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_statementfor_statement_declare的實現與while_statementdo_statement類似。 它們繼承自for_statement_base類,該類執行大部分邏輯。 當for循環的第一部分是變量聲明時,會創建for_statement_declare

C++ Stork:實現語句

這些都是我們擁有的語句類。 它們是我們功能的組成部分。 創建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(); ... };

函數loadtry_load將從給定路徑加載和編譯 Stork 腳本。 首先,其中一個可以拋出stork::error ,但第二個會捕獲它並將其打印在輸出上(如果提供)。

函數reset_globals將重新初始化全局變量。

函數add_external_functionscreate_public_function_caller應該在編譯之前被調用。 第一個添加了一個可以從 Stork 調用的 C++ 函數。 第二個創建可用於從 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; }

在編譯之前將標準函數添加到模塊中,並使用 Stork 腳本中的函數tracerndgreater的功能也被添加為展示。

該腳本是從文件“test.stk”中加載的,該文件與“main.cpp”位於同一文件夾中(通過使用__FILE__預處理器定義),然後調用函數main

在腳本中,我們生成一個隨機數組,使用比較器less升序排序,然後使用比較器greater的降序排序,用 C++ 編寫。

您可以看到代碼對於任何精通 C(或任何從 C 派生的編程語言)的人來說都是完全可讀的。

接下來做什麼?

我想在 Stork 中實現許多功能:

  • 結構
  • 類和繼承
  • 模塊間調用
  • Lambda 函數
  • 動態類型對象

缺乏時間和空間是我們尚未實施它們的原因之一。 當我在業餘時間實現新功能時,我將嘗試用新版本更新我的 GitHub 頁面。

包起來

我們創造了一種新的編程語言!

在過去的六周里,這佔用了我大部分空閒時間,但我現在可以編寫一些腳本並查看它們運行。 這幾天我就是這麼幹的,每次出乎意料的墜毀都撓著我的光頭。 有時,這是一個小錯誤,有時是一個討厭的錯誤。 然而,在其他時候,我感到很尷尬,因為這是一個我已經與世界分享的錯誤決定。 但每次,我都會修復並繼續編碼。

在這個過程中,我了解了我以前從未使用過的if constexpr 。 我也更加熟悉右值引用和完美轉發,以及我每天都不會遇到的 C++17 的其他較小功能。

代碼並不完美——我永遠不會這麼說——但它已經足夠好了,而且它主要遵循良好的編程實踐。 最重要的是 - 它有效。

決定從頭開始開發一門新語言對於普通人甚至普通程序員來說可能聽起來很瘋狂,但這樣做更有理由向自己證明你可以做到。 就像解決一個難題一樣,是一種很好的大腦鍛煉,可以保持精神健康。

枯燥的挑戰在我們的日常編程中很常見,因為我們不能只挑選其中有趣的方面,即使有時很無聊,也必須認真工作。 如果你是一名專業的開發人員,你的首要任務是向你的雇主提供高質量的代碼並將食物擺在桌面上。 這有時會使您避免在業餘時間進行編程,並且會降低您早期編程學生時代的熱情。

如果您不必這樣做,請不要失去這種熱情。 如果你覺得某件事很有趣,那就去做吧,即使它已經完成了。 你不必證明有一些樂趣的理由。

如果你能把它——甚至是部分——融入你的專業工作,對你有好處! 沒有多少人有這個機會。

這部分的代碼將被我的 GitHub 頁面上的一個專用分支凍結。