首頁 >後端開發 >php教程 >重新實現PHP中的範圍運算符

重新實現PHP中的範圍運算符

Christopher Nolan
Christopher Nolan原創
2025-02-15 09:36:12222瀏覽

SitePoint精彩文章推薦:改進後的PHP範圍運算符實現

本文經作者授權轉載於SitePoint。以下內容由Thomas Punt撰寫,介紹了PHP範圍運算符的改進實現方法。如果您對PHP內部機制和為喜愛的編程語言添加功能感興趣,那麼現在正是學習的好時機!

本文假設讀者能夠從源代碼構建PHP。如果不是這樣,請先閱讀PHP內部機製書籍的“構建PHP”章節。

Re-Implementing the Range Operator in PHP


在本文的前篇(提示:請確保您已閱讀),我展示了一種在PHP中實現範圍運算符的方法。然而,最初的實現很少是最好的,因此本文旨在探討如何改進之前的實現。

再次感謝Nikita Popov校對本文!

關鍵要點

  • Thomas Punt重新實現了PHP中的範圍運算符,將計算邏輯從Zend虛擬機中移出,從而允許在常量表達式上下文中使用範圍運算符。
  • 此重新實現能夠在編譯時(對於字面量操作數)或運行時(對於動態操作數)進行計算。這不僅為Opcache用戶帶來了一點好處,而且允許將常量表達式功能與範圍運算符一起使用。
  • 重新實現過程涉及更新詞法分析器、解析器、編譯階段和Zend虛擬機。詞法分析器實現保持不變,而解析器實現與之前部分相同。編譯階段不需要更新Zend/zend_compile.c文件,因為它已經包含處理二元運算的必要邏輯。 Zend虛擬機已更新為在運行時處理ZEND_RANGE操作碼的執行。
  • 在本系列的第三部分中,Punt計劃通過介紹如何重載此運算符來構建此實現。這將使對象能夠用作操作數,並為字符串添加適當的支持。

先前實現的缺點

最初的實現將範圍運算符的所有邏輯都放在Zend虛擬機中,這迫使計算在執行ZEND_RANGE操作碼時純粹在運行時進行。這不僅意味著對於字面量操作數,計算不能轉移到編譯時,而且還意味著某些功能根本無法工作。

在此實現中,我們將範圍運算符邏輯從Zend虛擬機中移出,以便能夠在編譯時(對於字面量操作數)或運行時(對於動態操作數)進行計算。這不僅為Opcache用戶帶來了一點好處,更重要的是允許將常量表達式功能與範圍運算符一起使用。

例如:

<code class="language-php">// 作为常量定义
const AN_ARRAY = 1 |> 100;

// 作为初始属性定义
class A
{
    private $a = 1 |> 2;
}

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}</code>

因此,事不宜遲,讓我們重新實現範圍運算符。

更新詞法分析器

詞法分析器實現保持完全不變。令牌首先在Zend/zend_language_scanner.l(約1200行)中註冊:

<code class="language-c"><st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}</code>

然後在Zend/zend_language_parser.y(約220行)中聲明:

<code class="language-php">// 作为常量定义
const AN_ARRAY = 1 |> 100;

// 作为初始属性定义
class A
{
    private $a = 1 |> 2;
}

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}</code>

必須再次通過進入ext/tokenizer目錄並執行tokenizer_data_gen.sh文件來重新生成標記器擴展。

更新解析器

解析器實現與之前部分相同。我們再次通過將T_RANGE令牌添加到以下行的末尾來聲明運算符的優先級和結合性(約70行):

<code class="language-c"><st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}</code>

然後,我們再次更新expr_without_variable產生式規則,但這次語義動作(花括號內的代碼)將略有不同。使用以下代碼更新它(我將其放在T_SPACESHIP規則下方,約930行):

<code class="language-c">%token T_RANGE           "|> (T_RANGE)"</code>

這次,我們使用了zend_ast_create_binary_op函數(而不是zend_ast_create函數),它為我們創建了一個ZEND_AST_BINARY_OP節點。 zend_ast_create_binary_op採用一個操作碼名稱,該名稱將在編譯階段用於區分二元運算。

由於我們現在正在重用ZEND_AST_BINARY_OP節點類型,因此無需像之前在Zend/zend_ast.h文件中那樣定義新的ZEND_AST_RANGE節點類型。

更新編譯階段

這次,無需更新Zend/zend_compile.c文件,因為它已經包含處理二元運算的必要邏輯。因此,我們只需通過將我們的運算符設為ZEND_AST_BINARY_OP節點來重用此邏輯。

以下是zend_compile_binary_op函數的簡化版本:

<code class="language-c">%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE</code>

正如我們所看到的,它與我們上次創建的zend_compile_range函數非常相似。兩個重要的區別在於如何獲取操作碼類型以及當兩個操作數都是字面量時會發生什麼。

操作碼類型這次是從AST節點獲取的(而不是像上次那樣硬編碼),因為ZEND_AST_BINARY_OP節點存儲此值(如新的產生式規則的語義動作所示)以區分二元運算。當兩個操作數都是字面量時,將調用zend_try_ct_eval_binary_op函數。此函數如下所示:

<code class="language-c">    |   expr T_RANGE expr
            { $$ = zend_ast_create_binary_op(ZEND_RANGE, , ); }</code>

該函數根據操作碼類型從Zend/zend_opcode.c中的get_binary_op函數(源代碼)獲取回調。這意味著我們需要接下來更新此函數以適應ZEND_RANGE操作碼。將以下case語句添加到get_binary_op函數(約750行):

<code class="language-c">void zend_compile_binary_op(znode *result, zend_ast *ast) /* {{{ */
{
    zend_ast *left_ast = ast->child[0];
    zend_ast *right_ast = ast->child[1];
    uint32_t opcode = ast->attr;

    znode left_node, right_node;
    zend_compile_expr(&left_node, left_ast);
    zend_compile_expr(&right_node, right_ast);

    if (left_node.op_type == IS_CONST && right_node.op_type == IS_CONST) {
        if (zend_try_ct_eval_binary_op(&result->u.constant, opcode,
                &left_node.u.constant, &right_node.u.constant)
        ) {
            result->op_type = IS_CONST;
            zval_ptr_dtor(&left_node.u.constant);
            zval_ptr_dtor(&right_node.u.constant);
            return;
        }
    }

    do {
        // redacted code
        zend_emit_op_tmp(result, opcode, &left_node, &right_node);
    } while (0);
}
/* }}} */</code>

現在我們必須定義range_function函數。這將在Zend/zend_operators.c文件中與所有其他運算符一起完成:

<code class="language-c">static inline zend_bool zend_try_ct_eval_binary_op(zval *result, uint32_t opcode, zval *op1, zval *op2) /* {{{ */
{
    binary_op_type fn = get_binary_op(opcode);

    /* don't evaluate division by zero at compile-time */
    if ((opcode == ZEND_DIV || opcode == ZEND_MOD) &&
        zval_get_long(op2) == 0) {
        return 0;
    } else if ((opcode == ZEND_SL || opcode == ZEND_SR) &&
        zval_get_long(op2)      return 0;
    }

    fn(result, op1, op2);
    return 1;
}
/* }}} */</code>

函數原型包含兩個新的宏:ZEND_API和ZEND_FASTCALL。 ZEND_API用於通過使函數可用於編譯為共享對象的擴展來控制函數的可見性。 ZEND_FASTCALL用於確保使用更高效的調用約定,其中前兩個參數將使用寄存器而不是堆棧傳遞(對於x86上的64位構建比32位構建更相關)。

函數體與我們在上一篇文章中的Zend/zend_vm_def.h文件中所擁有的非常相似。 VM特定的內容不再存在,包括HANDLE_EXCEPTION宏調用(已替換為return FAILURE;),並且ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION宏調用已被完全刪除(此檢查和操作需要保留在VM中,因此宏將稍後從VM代碼中調用)。此外,如前所述,我們避免使用GET_OPn_ZVAL_PTR偽宏(而不是GET_OPn_ZVAL_PTR_DEREF)在VM中處理引用。

另一個值得注意的區別是我們正在對兩個操作數應用ZVAL_DEFEF以確保正確處理引用。這以前是在VM內部使用偽宏GET_OPn_ZVAL_PTR_DEREF完成的,但現在已轉移到此函數中。這樣做不是因為它在編譯時需要(因為對於編譯時處理,兩個操作數都必須是字面量,並且它們不能被引用),而是因為它使代碼庫中的其他位置能夠安全地調用range_function,而無需擔心引用處理。因此,大多數運算符函數(除了性能至關重要的地方)都執行引用處理,而不是在其VM操作碼定義中執行。

最後,我們必須將range_function原型添加到Zend/zend_operators.h文件:

<code class="language-php">// 作为常量定义
const AN_ARRAY = 1 |> 100;

// 作为初始属性定义
class A
{
    private $a = 1 |> 2;
}

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}</code>

更新Zend虛擬機

現在我們必須再次更新Zend虛擬機以在運行時處理ZEND_RANGE操作碼的執行。將以下代碼放在Zend/zend_vm_def.h(底部):

<code class="language-c"><st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}</code>

(同樣,操作碼編號必須比當前最高操作碼編號大一,這可以在Zend/zend_vm_opcodes.h文件的底部看到。)

這次的定義要短得多,因為所有工作都在range_function中處理。我們只需調用此函數,傳入當前opline的結果操作數即可保存計算值。從range_function中刪除的異常檢查和跳到下一個操作碼仍在VM中由對ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION的調用處理。此外,如前所述,我們避免使用GET_OPn_ZVAL_PTR偽宏(而不是GET_OPn_ZVAL_PTR_DEREF)在VM中處理引用。

現在通過執行Zend/zend_vm_gen.php文件重新生成VM。

最後,漂亮打印機需要再次更新Zend/zend_ast.c文件。通過指定新運算符的優先級為170來更新優先級表註釋(約520行):

<code class="language-c">%token T_RANGE           "|> (T_RANGE)"</code>

然後,在zend_ast_export_ex函數中插入一個case語句,以在ZEND_AST_BINARY_OP case語句中處理ZEND_RANGE操作碼(約1300行):

<code class="language-c">%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE</code>

結論

本文展示了一種實現範圍運算符的替代方法,其中計算邏輯已從VM中移出。這具有能夠在常量表達式上下文中使用範圍運算符的優點。

本系列文章的第三部分將在此實現的基礎上構建,介紹如何重載此運算符。這將允許對像用作操作數(例如來自GMP庫的對像或實現__toString方法的對象)。它還將展示如何為字符串添加適當的支持(不像PHP當前範圍函數中看到的支持)。但就目前而言,我希望這能很好地演示ZE在將運算符實現到PHP中時的一些更深層次的方面。

以上是重新實現PHP中的範圍運算符的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn