Stork,第 3 部分:實現表達式和變量

已發表: 2022-03-11

在我們系列的第 3 部分中,我們的輕量級編程語言最終將運行。 它不會是圖靈完備的,它不會很強大,但它將能夠評估表達式,甚至調用用 C++ 編寫的外部函數。

我將嘗試盡可能詳細地描述該過程,主要是因為這是本系列博客的目的,但也是為了我自己的文檔,因為在這一部分中,事情變得有些複雜。

我在第二篇文章發表之前就開始為這部分編碼,但後來發現表達式解析器應該是一個獨立的組件,值得擁有自己的博客文章。

再加上一些臭名昭著的編程技術,這使得這部分可能不會太大,然而,一些讀者很可能會指出上述編程技術並想知道為什麼我必須使用它們。

為什麼我們使用宏?

隨著我在不同項目和不同人的工作中獲得編程經驗,我了解到開發人員往往非常教條——可能是因為這樣更容易。

C++ 中的宏

編程的第一個教條是goto語句是壞的、邪惡的和可怕的。 我可以理解這種情緒的來源,並且在絕大多數情況下,當有人使用goto語句時,我同意這個概念。 它通常可以避免,而是可以編寫更具可讀性的代碼。

然而,不可否認的是,在 C++ 中,使用goto語句可以很容易地打破內循環。 另一種方法——需要一個bool變量或一個專用函數——可能比教條地落入被禁止的編程技術桶中的代碼可讀性差。

專門針對 C 和 C++ 開發人員的第二個信條是,宏是壞的、邪惡的、可怕的,而且基本上是一場等待發生的災難。 這幾乎總是伴隨著這個例子:

 #define max(a, b) ((a) > (b) ? (a) : (b)) ... int x = 3; int z = 2; int y = max(x++, z);

然後有一個問題:在這段代碼之後, x的值是多少,答案是5 ,因為x增加了兩次,在 ? 的兩邊各增加一次? -操作員。

唯一的問題是在這種情況下沒有人使用宏。 如果在普通函數可以正常工作的場景中使用宏,尤其是當它們偽裝成函數時,那麼宏就是邪惡的,因此用戶不會意識到它們的副作用。 但是,我們不會將它們用作函數,我們將使用大寫字母作為它們的名稱,以表明它們不是函數。 我們將無法正確調試它們,這很糟糕,但我們會忍受這種情況,因為另一種方法是複制粘貼相同的代碼數十次,這比宏更容易出錯。 該問題的一種解決方案是編寫代碼生成器,但是當我們已經在 C++ 中嵌入了一個代碼生成器時,為什麼還要編寫它呢?

編程中的教條幾乎總是不好的。 我在這裡謹慎地使用“幾乎”只是為了避免遞歸地陷入我剛剛設置的教條陷阱。

你可以在這裡找到這部分的代碼和所有宏。

變量

在上一部分中,我提到過 Stork 不會被編譯成二進製或類似彙編語言的任何東西,但我也說過它將是一種靜態類型的語言。 因此,它將被編譯,但在一個能夠執行的 C++ 對像中。 稍後會變得更清楚,但現在,讓我們聲明所有變量都將是它們自己的對象。

由於我們希望將它們保存在全局變量容器或堆棧中,一種方便的方法是定義基類並從其繼承。

 class variable; using variable_ptr = std::shared_ptr<variable>; class variable: public std::enable_shared_from_this<variable> { private: variable(const variable&) = delete; void operator=(const variable&) = delete; protected: variable() = default; public: virtual ~variable() = default; virtual variable_ptr clone() const = 0; template <typename T> T static_pointer_downcast() { return std::static_pointer_cast< variable_impl<typename T::element_type::value_type> >(shared_from_this()); } };

如您所見,它相當簡單,並且執行深層複製的函數clone是它除了析構函數之外的唯一虛成員函數。

由於我們將始終通過shared_ptr使用此類的對象,因此從std::enable_shared_from_this繼承它是有意義的,這樣我們就可以輕鬆地從中獲取共享指針。 函數static_pointer_downcast在這裡是為了方便,因為我們經常不得不從這個類向下轉換到它的實現。

這個類的真正實現是variable_impl ,參數化了它所持有的類型。 它將為我們將使用的四種類型實例化:

 using number = double; using string = std::shared_ptr<std::string>; using array = std::deque<variable_ptr>; using function = std::function<void(runtime_context&)>;

我們將使用double作為我們的數字類型。 字符串是引用計數的,因為它們是不可變的,以便在按值傳遞它們時啟用某些優化。 數組將是std::deque ,因為它是穩定的,我們只需注意runtime_context是在運行時保存有關程序內存的所有相關信息的類。 我們稍後會談到。

以下定義也經常使用:

 using lvalue = variable_ptr; using lnumber = std::shared_ptr<variable_impl<number>>; using lstring = std::shared_ptr<variable_impl<string>>; using larray = std::shared_ptr<variable_impl<array>>; using lfunction = std::shared_ptr<variable_impl<function>>;

這裡使用的“l”是“lvalue”的縮寫。 每當我們有某種類型的左值時,我們將使用指向variable_impl的共享指針。

運行時上下文

在運行時,內存狀態保存在類runtime_context

 class runtime_context{ private: std::vector<variable_ptr> _globals; std::deque<variable_ptr> _stack; std::stack<size_t> _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函數將堆棧大小調整一併將_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<lnumber>(); } lstring evaluate_lstring(runtime_context& context) const { return evaluate(context)->static_pointer_downcast<lstring>(); } 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<expression>;

然後我們將從這個類中繼承每個操作,例如加法、連接、函數調用等。例如,這將是加法表達式的實現:

 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<variable_impl<number> >( _expr1->evaluate_number(context) + _expr2->evaluate_number(context) ); } ... };

所以我們需要評估兩邊( _expr1_expr2 ),將它們相加,然後構造variable_impl<number>

我們可以安全地向下轉換變量,因為我們在編譯時檢查了它們的類型,所以這不是這裡的問題。 然而,最大的問題是我們為返回對象的堆分配付出的性能損失,理論上,這是不需要的。 我們這樣做是為了滿足虛函數聲明。 在 Stork 的第一個版本中,當我們從函數返回數字時,我們將受到懲罰。 我可以接受,但不能接受簡單的預增量表達式進行堆分配。

然後,我嘗試使用從公共基礎繼承的特定於類型的表達式:

 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<lvalue_expression>; 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<number_expression>; 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<lnumber_expression>;

這只是層次結構的一部分(僅適用於數字),我們已經遇到了菱形的問題(類繼承了具有相同基類的兩個類)。

幸運的是,C++ 提供了虛擬繼承,它提供了從基類繼承的能力,方法是將指向它的指針保留在被繼承的類中。 因此,如果類 B 和 C 虛擬繼承自 A,而類 D 繼承自 B 和 C,則 D 中將只有一個 A 的副本。

不過,在這種情況下,我們必須付出一些代價——性能和無法從 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); } ... };

在語法方面,沒有什麼可要求的了,這很自然。 但是,如果任何內部表達式是左值數字表達式,則需要兩個虛函數調用來計算它。 不完美,但也不可怕。

讓我們將字符串添加到這個組合中,看看它會把我們帶到哪裡:

 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<string_expression>;

由於我們希望將數字轉換為字符串,因此我們需要從string_expression繼承number_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<number_expression>;

我們倖免於難,但我們必須重新覆蓋evaluate虛擬方法,否則由於從數字到字符串的不必要轉換,我們將面臨嚴重的性能問題。

所以,事情顯然變得醜陋了,我們的設計幾乎沒有倖免於難,因為我們沒有兩種類型的表達式應該相互轉換(兩種方式)。 如果是這種情況,或者如果我們嘗試進行任何類型的循環轉換,我們的層次結構將無法處理它。 畢竟,層次結構應該反映 is-a 關係,而不是 is-convertible-to 關係,後者更弱。

所有這些不成功的嘗試都讓我做出了一個複雜但——在我看來——合適的設計。 首先,擁有一個單一的基類對我們來說並不重要。 我們需要計算結果為 void 的表達式類,但如果我們可以在編譯時區分 void 表達式和其他類型的表達式,則無需在運行時在它們之間進行轉換。 因此,我們將使用表達式的返回類型參數化基類。

這是該類的完整實現:

 template <typename R> class expression { expression(const expression&) = delete; void operator=(const expression&) = delete; protected: expression() = default; public: using ptr = std::unique_ptr<const expression>; virtual R evaluate(runtime_context& context) const = 0; virtual ~expression() = default; };

每次表達式求值我們將只有一個虛函數調用(當然,我們將不得不遞歸調用它),並且由於我們不編譯為二進制代碼,所以這是一個很好的結果。 唯一要做的就是在允許的情況下進行類型之間的轉換。

為此,我們將使用返回類型對每個表達式進行參數化,並從相應的基類繼承它。 然後,在evaluate函數中,我們將評估結果轉換為該函數的返回值。

例如,這是我們的加法表達式:

 template <typename R> class add_expression: public expression<R> { ... R evaluate(runtime_context& context) const override{ return convert<R>( _expr1->evaluate(context) + _expr2->evaluate(context) ); } ... };

要編寫“轉換”函數,我們需要一些基礎設施:

 template<class V, typename T> struct is_boxed { static const bool value = false; }; template<typename T> struct is_boxed<std::shared_ptr<variable_impl<T> >, 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::string>(std::move(str)); } string convert_to_string(const lnumber& v) { return convert_to_string(v->value); }

結構is_boxed是一個類型特徵,它有一個內部常量value ,當(且僅當)第一個參數是指向variable_impl參數化的第二個類型的共享指針時,它的計算結果為 true。

即使在舊版本的 C++ 中也可以實現convert函數,但是在 C++17 中有一個非常有用的語句,稱為if constexpr ,它在編譯時評估條件。 如果它評估為false ,它將完全刪除該塊,即使它會導致編譯時錯誤。 否則,它將丟棄else塊。

 template<typename To, typename From> auto convert(From&& from) { if constexpr(std::is_convertible<From, To>::value) { return std::forward<From>(from); } else if constexpr(is_boxed<From, To>::value) { return unbox(std::forward<From>(from)); } else if constexpr(std::is_same<To, string>::value) { return convert_to_string(from); } else { static_assert(std::is_void<To>::value); } }

嘗試閱讀此功能:

  • 如果它在 C++ 中是可轉換的,則轉換(這是用於variable_impl指針向上轉換)。
  • 如果已裝箱,請取消裝箱。
  • 如果目標類型是字符串,則轉換為字符串。
  • 什麼都不做,檢查目標是否無效。

在我看來,這比基於 SFINAE 的舊語法更具可讀性。

我將提供表達式類型的簡要概述並省略一些技術細節以使其保持合理的簡短。

表達式樹中存在三種類型的葉表達式:

  • 全局變量表達式
  • 局部變量表達式
  • 常量表達式
template<typename R, typename T> class global_variable_expression: public expression<R> { private: int _idx; public: global_variable_expression(int idx) : _idx(idx) { } R evaluate(runtime_context& context) const override { return convert<R>( context.global(_idx) ->template static_pointer_downcast<T>() ); } };

除了返回類型外,它還使用變量類型進行參數化。 局部變量的處理方式類似,這是常量類:

 template<typename R, typename T> class constant_expression: public expression<R> { private: T _c; public: constant_expression(T c) : _c(std::move(c)) { } R evaluate(runtime_context& context) const override { return convert<R>(_c); } };

在這種情況下,我們立即在構造函數中轉換常量。

這被用作我們大多數表達式的基類:

 template<class O, typename R, typename... Ts> class generic_expression: public expression<R> { private: std::tuple<typename expression<Ts>::ptr...> _exprs; template<typename... Exprs> R evaluate_tuple( runtime_context& context, const Exprs&... exprs ) const { return convert<R>(O()( std::move(exprs->evaluate(context))...) ); } public: generic_expression(typename expression<Ts>::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 ); } };

第一個參數是將被實例化並調用以進行評估的函子類型。 其餘類型是子表達式的返回類型。

為了減少樣板代碼,我們定義了三個宏:

 #define UNARY_EXPRESSION(name, code)\ struct name##_op {\ template <typename T1> \ auto operator()(T1 t1) {\ code;\ }\ };\ template<typename R, typename T1>\ using name##_expression = generic_expression<name##_op, R, T1>; #define BINARY_EXPRESSION(name, code)\ struct name##_op {\ template <typename T1, typename T2>\ auto operator()(T1 t1, T2 t2) {\ code;\ }\ };\ template<typename R, typename T1, typename T2>\ using name##_expression = generic_expression<name##_op, R, T1, T2>; #define TERNARY_EXPRESSION(name, code)\ struct name##_op {\ template <typename T1, typename T2, typename T3>\ auto operator()(T1 t1, T2 t2, T3 t3) {\ code;\ }\ };\ template<typename R, typename T1, typename T2, typename T3>\ using name##_expression = generic_expression<name##_op, R, T1, T2, T3>;

請注意, operator()被定義為模板,儘管通常不必如此。 以相同的方式定義所有表達式而不是提供參數類型作為宏參數更容易。

現在,我們可以定義大部分錶達式。 例如,這是/=的定義:

 BINARY_EXPRESSION(div_assign, t1->value /= t2; return t1; );

我們可以使用這些宏定義幾乎所有的表達式。 例外是定義了參數求值順序的運算符(邏輯&&|| 、三元 ( ? ) 和逗號 ( , ) 運算符)、數組索引、函數調用和param_expression ,它們克隆參數以將其傳遞給函數按價值。

這些的實現沒有什麼複雜的。 函數調用實現是最複雜的,所以我在這裡解釋一下:

 template<typename R, typename T> class call_expression: public expression<R>{ private: expression<function>::ptr _fexpr; std::vector<expression<lvalue>::ptr> _exprs; public: call_expression( expression<function>::ptr fexpr, std::vector<expression<lvalue>::ptr> exprs ): _fexpr(std::move(fexpr)), _exprs(std::move(exprs)) { } R evaluate(runtime_context& context) const override { std::vector<variable_ptr> params; params.reserve(_exprs.size()); for (size_t i = 0; i < _exprs.size(); ++i) { params.push_back(_exprs[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<R, void>::value) { context.end_function(_exprs.size()); } else { return convert<R>( context.end_function( _exprs.size() )->template static_pointer_downcast<T>() ); } } };

它通過將所有評估參數壓入其堆棧並調用call函數來準備runtime_context 。 然後它調用評估的第一個參數(即函數本身)並返回end_function方法的返回值。 我們也可以在這裡看到if constexpr語法的用法。 它使我們不必為返回void的函數編寫整個類的特化。

現在,我們擁有了與運行時可用的表達式相關的所有內容。 唯一剩下的就是從解析的表達式樹(在上一篇博客文章中描述)到表達式樹的轉換。

表達式生成器

為避免混淆,讓我們命名語言開發週期的不同階段:

編程語言開發週期的不同階段
  • Meta-compile-time:C++編譯器運行的階段
  • Compile-time:Stork 編譯器運行的階段
  • 運行時:Stork 腳本運行的階段

這是表達式生成器的偽代碼:

 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) ...

第一個和第三個表達式的返回類型為void ,因為我們不對它們的求值結果做任何事情。 然而,第二個表達式有一個類型number ,因為我們將它與零進行比較,以決定是否停止循環。

如果我們知道與節點操作相關的表達式的類型,它通常會確定其子表達式的類型。

例如,如果表達式(expression1) += (expression2)的類型為lnumber ,這意味著expression1也具有該類型,而expression2的類型number

但是,表達式(expression1) < (expression2)始終具有類型number ,但它們的子表達式可以具有類型number或類型string 。 在這個表達式的情況下,我們將檢查兩個節點是否都是數字。 如果是這樣,我們將expression1expression2構建為expression<number> 。 否則,它們將是expression<string>類型。

還有一個問題是我們必須考慮和處理的。

想像一下,如果我們需要構建一個number類型的表達式。 然後,如果遇到連接運算符,我們將無法返回任何有效的東西。 我們知道這不可能發生,因為我們在構建表達式樹時已經檢查了類型(在上一部分中),但這意味著我們不能編寫模板函數,用返回類型參數化,因為它會有無效的分支取決於在該返回類型上。

一種方法是使用if constexpr按返回類型拆分函數,但效率低下,因為如果相同的操作存在於多個分支中,我們將不得不重複其代碼。 在這種情況下,我們可以編寫單獨的函數。

實現的解決方案根據節點類型拆分功能。 在每個分支中,我們將檢查該分支類型是否可轉換為函數返回類型。 如果不是,我們將拋出編譯器錯誤,因為它不應該發生,但是代碼對於如此強烈的聲明來說太複雜了。 我可能犯了一個錯誤。

我們使用以下不言自明的類型特徵結構來檢查可轉換性:

 template<typename From, typename To> struct is_convertible { static const bool value = std::is_convertible<From, To>::value || is_boxed<From, To>::value || ( std::is_same<To, string>::value && ( std::is_same<From, number>::value || std::is_same<From, lnumber>::value ) ); };

拆分之後,代碼幾乎很簡單。 我們可以在語義上從原始表達式類型轉換為我們想要構建的類型,並且元編譯時沒有錯誤。

然而,有很多樣板代碼,所以我嚴重依賴宏來減少它。

 template<typename R> class expression_builder{ private: using expression_ptr = typename expression<R>::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<T, R>::value) {\ return build_##T##_expression(np, context);\ } else {\ throw expression_builder_error();\ return expression_ptr();\ }

我們必須在 else-branch 中返回空指針,因為在不可能轉換的情況下編譯器無法知道函數的返回類型; 否則, std::visit要求所有重載函數具有相同的返回類型。

例如,有一個函數使用string作為返回類型來構建表達式:

 static expression_ptr build_string_expression( const node_ptr& np, compiler_context& context ) { if (std::holds_alternative<std::string>(np->get_value())) { return std::make_unique<constant_expression<R, string>>( std::make_shared<std::string>( std::get<std::string>(np->get_value()) ) ); } CHECK_IDENTIFIER(lstring); switch (std::get<node_operation>(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_IDENTIFIERCHECK_BINARY_OPERATION宏的實現:

 #define CHECK_IDENTIFIER(T1)\ if (std::holds_alternative<identifier>(np->get_value())) {\ const identifier& id = std::get<identifier>(np->get_value());\ const identifier_info* info = context.find(id.name);\ if (info->is_global()) {\ return std::make_unique<\ global_variable_expression<R, T1>\ >(info->index());\ } else {\ return std::make_unique<\ local_variable_expression<R, T1>\ >(info->index());\ }\ }
 #define CHECK_BINARY_OPERATION(name, T1, T2)\ case node_operation::name:\ return expression_ptr(\ std::make_unique<name##_expression<R, T1, T2> > (\ expression_builder<T1>::build_expression(\ np->get_children()[0], context\ ),\ expression_builder<T2>::build_expression(\ np->get_children()[1], context\ )\ )\ );

CHECK_IDENTIFIER宏必須參考compiler_context來構建具有適當索引的全局或局部變量表達式。 這是此結構中compiler_context的唯一用法。

您可以看到CHECK_BINARY_OPERATION遞歸地為子節點調用build_expression

包起來

在我的 GitHub 頁面上,您可以獲得完整的源代碼,編譯它,然後輸入表達式並查看評估變量的結果。

我想,在人類創造力的所有分支中,都有一段時間作者意識到他們的產品在某種意義上是有生命的。 在編程語言的構建中,你可以看到語言“呼吸”的時刻。

在本系列的下一部分和最後一部分中,我們將實現其餘的最小語言功能集,以使其實時運行。