From d363881c66bf4a301bb0af9c5b8119e50f2a7494 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 10 Jul 2026 12:39:27 +0200 Subject: [PATCH] Compile negated conditions as inverted jumps 'if (!$x)', while/do-while conditions and ternaries with a top-level '!' emitted a BOOL_NOT into a temporary followed by JMPZ/JMPNZ - two dispatches and a TMP per evaluation, since without the opcache optimizer nothing rewrites it. Strip '!' layers in the condition and flip the jump opcode instead; both paths evaluate the operand with i_zend_is_true, so behavior including undefined-variable warnings is identical. This also lets comparisons under '!' fuse with the jump (smart branch), which BOOL_NOT previously prevented. Adjusts two opcode-dump tests for the changed temporary numbering. --- Zend/zend_compile.c | 30 ++++++++++++++++++++++++---- ext/opcache/tests/opt/gh18107_1.phpt | 10 +++++----- ext/opcache/tests/opt/gh18107_2.phpt | 12 +++++------ 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index c7ac93f8d34e..d0ce0739b44e 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2432,6 +2432,20 @@ static inline uint32_t zend_emit_cond_jump(uint8_t opcode, znode *cond, uint32_t } /* }}} */ +/* Compile `!` in a condition as an inverted jump instead of a BOOL_NOT + * opcode followed by a jump. Both evaluate the operand with the same + * boolean semantics, and removing the intermediate temporary also lets + * comparisons fuse with the jump (smart branch). */ +static zend_ast *zend_unwrap_bool_not(zend_ast *cond_ast, uint8_t *jump_opcode) /* {{{ */ +{ + while (cond_ast->kind == ZEND_AST_UNARY_OP && cond_ast->attr == ZEND_BOOL_NOT) { + *jump_opcode = (*jump_opcode == ZEND_JMPZ) ? ZEND_JMPNZ : ZEND_JMPZ; + cond_ast = cond_ast->child[0]; + } + return cond_ast; +} +/* }}} */ + static inline void zend_update_jump_target(uint32_t opnum_jump, uint32_t opnum_target) /* {{{ */ { zend_op *opline = &CG(active_op_array)->opcodes[opnum_jump]; @@ -6448,9 +6462,11 @@ static void zend_compile_while(const zend_ast *ast) /* {{{ */ opnum_cond = get_next_op_number(); zend_update_jump_target(opnum_jmp, opnum_cond); + uint8_t jump_opcode = ZEND_JMPNZ; + cond_ast = zend_unwrap_bool_not(cond_ast, &jump_opcode); zend_compile_expr(&cond_node, cond_ast); - zend_emit_cond_jump(ZEND_JMPNZ, &cond_node, opnum_start); + zend_emit_cond_jump(jump_opcode, &cond_node, opnum_start); zend_end_loop(opnum_cond, NULL); } @@ -6470,9 +6486,11 @@ static void zend_compile_do_while(const zend_ast *ast) /* {{{ */ zend_compile_stmt(stmt_ast); opnum_cond = get_next_op_number(); + uint8_t jump_opcode = ZEND_JMPNZ; + cond_ast = zend_unwrap_bool_not(cond_ast, &jump_opcode); zend_compile_expr(&cond_node, cond_ast); - zend_emit_cond_jump(ZEND_JMPNZ, &cond_node, opnum_start); + zend_emit_cond_jump(jump_opcode, &cond_node, opnum_start); zend_end_loop(opnum_cond, NULL); } @@ -6661,8 +6679,10 @@ static void zend_compile_if(zend_ast *ast) /* {{{ */ zend_do_extended_stmt(NULL); } + uint8_t jump_opcode = ZEND_JMPZ; + cond_ast = zend_unwrap_bool_not(cond_ast, &jump_opcode); zend_compile_expr(&cond_node, cond_ast); - opnum_jmpz = zend_emit_cond_jump(ZEND_JMPZ, &cond_node, 0); + opnum_jmpz = zend_emit_cond_jump(jump_opcode, &cond_node, 0); zend_compile_stmt(stmt_ast); @@ -11128,9 +11148,11 @@ static void zend_compile_conditional(znode *result, zend_ast *ast) /* {{{ */ return; } + uint8_t jump_opcode = ZEND_JMPZ; + cond_ast = zend_unwrap_bool_not(cond_ast, &jump_opcode); zend_compile_expr(&cond_node, cond_ast); - opnum_jmpz = zend_emit_cond_jump(ZEND_JMPZ, &cond_node, 0); + opnum_jmpz = zend_emit_cond_jump(jump_opcode, &cond_node, 0); zend_compile_expr(&true_node, true_ast); diff --git a/ext/opcache/tests/opt/gh18107_1.phpt b/ext/opcache/tests/opt/gh18107_1.phpt index a2cf3162090c..0ebe16e7b061 100644 --- a/ext/opcache/tests/opt/gh18107_1.phpt +++ b/ext/opcache/tests/opt/gh18107_1.phpt @@ -29,16 +29,16 @@ $_main: ; %s 0000 T1 = ISSET_ISEMPTY_CV (isset) CV0($badvar) 0001 JMPNZ T1 0006 -0002 T3 = NEW 1 string("Exception") +0002 T2 = NEW 1 string("Exception") 0003 SEND_VAL%S string("Should happen") 1 0004 DO_FCALL -0005 THROW T3 +0005 THROW T2 0006 JMP 0006 -0007 T6 = NEW 1 string("Exception") +0007 T5 = NEW 1 string("Exception") 0008 SEND_VAL%S string("Should not happen") 1 0009 DO_FCALL -0010 THROW T6 -0011 FAST_RET T5 +0010 THROW T5 +0011 FAST_RET T4 EXCEPTION TABLE: 0006, -, 0007, 0011 diff --git a/ext/opcache/tests/opt/gh18107_2.phpt b/ext/opcache/tests/opt/gh18107_2.phpt index 74a709223e5a..abd5d573037c 100644 --- a/ext/opcache/tests/opt/gh18107_2.phpt +++ b/ext/opcache/tests/opt/gh18107_2.phpt @@ -32,19 +32,19 @@ $_main: ; %s 0000 T2 = ISSET_ISEMPTY_CV (isset) CV0($badvar) 0001 JMPNZ T2 0008 -0002 T4 = NEW 1 string("Exception") +0002 T3 = NEW 1 string("Exception") 0003 SEND_VAL%S string("Should happen") 1 0004 DO_FCALL -0005 THROW T4 +0005 THROW T3 0006 CV1($e) = CATCH string("Throwable") 0007 ECHO string("foo") -0008 T6 = FAST_CALL 0010 +0008 T5 = FAST_CALL 0010 0009 JMP 0015 -0010 T7 = NEW 1 string("Exception") +0010 T6 = NEW 1 string("Exception") 0011 SEND_VAL%S string("Should not happen") 1 0012 DO_FCALL -0013 THROW T7 -0014 FAST_RET T6 +0013 THROW T6 +0014 FAST_RET T5 0015 RETURN int(1) EXCEPTION TABLE: 0006, 0006, 0010, 0014