首页 > 后端开发 > php教程 > 重新实现PHP中的范围运算符

重新实现PHP中的范围运算符

Christopher Nolan
发布: 2025-02-15 09:36:12
原创
211 人浏览过

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用户带来了一点好处,更重要的是允许将常量表达式功能与范围运算符一起使用。

例如:

// 作为常量定义
const AN_ARRAY = 1 |> 100;

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

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}
登录后复制
登录后复制
登录后复制

因此,事不宜迟,让我们重新实现范围运算符。

更新词法分析器

词法分析器实现保持完全不变。令牌首先在Zend/zend_language_scanner.l(约1200行)中注册:

<st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}
登录后复制
登录后复制
登录后复制

然后在Zend/zend_language_parser.y(约220行)中声明:

// 作为常量定义
const AN_ARRAY = 1 |> 100;

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

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}
登录后复制
登录后复制
登录后复制

必须再次通过进入ext/tokenizer目录并执行tokenizer_data_gen.sh文件来重新生成标记器扩展。

更新解析器

解析器实现与之前部分相同。我们再次通过将T_RANGE令牌添加到以下行的末尾来声明运算符的优先级和结合性(约70行):

<st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}
登录后复制
登录后复制
登录后复制

然后,我们再次更新expr_without_variable产生式规则,但这次语义动作(花括号内的代码)将略有不同。使用以下代码更新它(我将其放在T_SPACESHIP规则下方,约930行):

%token T_RANGE           "|> (T_RANGE)"
登录后复制
登录后复制

这次,我们使用了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函数的简化版本:

%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE
登录后复制
登录后复制

正如我们所看到的,它与我们上次创建的zend_compile_range函数非常相似。两个重要的区别在于如何获取操作码类型以及当两个操作数都是字面量时会发生什么。

操作码类型这次是从AST节点获取的(而不是像上次那样硬编码),因为ZEND_AST_BINARY_OP节点存储此值(如新的产生式规则的语义动作所示)以区分二元运算。当两个操作数都是字面量时,将调用zend_try_ct_eval_binary_op函数。此函数如下所示:

    |   expr T_RANGE expr
            { $$ = zend_ast_create_binary_op(ZEND_RANGE, , ); }
登录后复制

该函数根据操作码类型从Zend/zend_opcode.c中的get_binary_op函数(源代码)获取回调。这意味着我们需要接下来更新此函数以适应ZEND_RANGE操作码。将以下case语句添加到get_binary_op函数(约750行):

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);
}
/* }}} */
登录后复制

现在我们必须定义range_function函数。这将在Zend/zend_operators.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;
}
/* }}} */
登录后复制

函数原型包含两个新的宏: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文件:

// 作为常量定义
const AN_ARRAY = 1 |> 100;

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

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}
登录后复制
登录后复制
登录后复制

更新Zend虚拟机

现在我们必须再次更新Zend虚拟机以在运行时处理ZEND_RANGE操作码的执行。将以下代码放在Zend/zend_vm_def.h(底部):

<st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}
登录后复制
登录后复制
登录后复制

(同样,操作码编号必须比当前最高操作码编号大一,这可以在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行):

%token T_RANGE           "|> (T_RANGE)"
登录后复制
登录后复制

然后,在zend_ast_export_ex函数中插入一个case语句,以在ZEND_AST_BINARY_OP case语句中处理ZEND_RANGE操作码(约1300行):

%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE
登录后复制
登录后复制

结论

本文展示了一种实现范围运算符的替代方法,其中计算逻辑已从VM中移出。这具有能够在常量表达式上下文中使用范围运算符的优点。

本系列文章的第三部分将在此实现的基础上构建,介绍如何重载此运算符。这将允许对象用作操作数(例如来自GMP库的对象或实现__toString方法的对象)。它还将展示如何为字符串添加适当的支持(不像PHP当前范围函数中看到的支持)。但就目前而言,我希望这能很好地演示ZE在将运算符实现到PHP中时的一些更深层次的方面。

以上是重新实现PHP中的范围运算符的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板