Skip to content

Implement explicit send-by-ref #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Zend/Optimizer/optimize_func_calls.c
Original file line number Diff line number Diff line change
@@ -340,6 +340,13 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
break;
}
break;
case ZEND_SEND_EXPLICIT_REF:
if (call_stack[call - 1].func) {
if (ARG_SHOULD_BE_SENT_BY_REF(call_stack[call - 1].func, opline->op2.num)) {
opline->opcode = ZEND_SEND_REF;
}
}
break;
case ZEND_SEND_UNPACK:
case ZEND_SEND_USER:
case ZEND_SEND_ARRAY:
2 changes: 2 additions & 0 deletions Zend/Optimizer/sccp.c
Original file line number Diff line number Diff line change
@@ -238,6 +238,8 @@ static bool can_replace_op1(
case ZEND_UNSET_DIM:
case ZEND_UNSET_OBJ:
case ZEND_SEND_REF:
case ZEND_SEND_EXPLICIT_REF:
case ZEND_SEND_EXPLICIT_REF_FUNC:
case ZEND_SEND_VAR_EX:
case ZEND_SEND_FUNC_ARG:
case ZEND_SEND_UNPACK:
2 changes: 2 additions & 0 deletions Zend/Optimizer/zend_call_graph.c
Original file line number Diff line number Diff line change
@@ -139,6 +139,8 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
case ZEND_SEND_VAR_EX:
case ZEND_SEND_FUNC_ARG:
case ZEND_SEND_REF:
case ZEND_SEND_EXPLICIT_REF:
case ZEND_SEND_EXPLICIT_REF_FUNC:
case ZEND_SEND_VAR_NO_REF:
case ZEND_SEND_VAR_NO_REF_EX:
case ZEND_SEND_USER:
1 change: 1 addition & 0 deletions Zend/Optimizer/zend_dfg.c
Original file line number Diff line number Diff line change
@@ -154,6 +154,7 @@ static zend_always_inline void _zend_dfg_add_use_def_op(const zend_op_array *op_
case ZEND_BIND_INIT_STATIC_OR_JMP:
case ZEND_SEND_VAR_NO_REF:
case ZEND_SEND_VAR_NO_REF_EX:
case ZEND_SEND_EXPLICIT_REF:
case ZEND_SEND_VAR_EX:
case ZEND_SEND_FUNC_ARG:
case ZEND_SEND_REF:
4 changes: 4 additions & 0 deletions Zend/Optimizer/zend_inference.c
Original file line number Diff line number Diff line change
@@ -3268,6 +3268,7 @@ static zend_always_inline zend_result _zend_update_type_info(
}
break;
case ZEND_SEND_REF:
case ZEND_SEND_EXPLICIT_REF:
if (ssa_op->op1_def >= 0) {
tmp = MAY_BE_REF|MAY_BE_RC1|MAY_BE_RCN|MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF;
UPDATE_SSA_TYPE(tmp, ssa_op->op1_def);
@@ -3658,6 +3659,7 @@ static zend_always_inline zend_result _zend_update_type_info(
case ZEND_SEND_VAR_NO_REF:
case ZEND_SEND_VAR_NO_REF_EX:
case ZEND_SEND_REF:
case ZEND_SEND_EXPLICIT_REF:
case ZEND_ASSIGN_REF:
case ZEND_YIELD:
case ZEND_INIT_ARRAY:
@@ -4930,6 +4932,7 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
case ZEND_FETCH_DIM_IS:
case ZEND_FETCH_OBJ_IS:
case ZEND_SEND_REF:
case ZEND_SEND_EXPLICIT_REF:
case ZEND_UNSET_CV:
case ZEND_ISSET_ISEMPTY_CV:
case ZEND_MAKE_REF:
@@ -4958,6 +4961,7 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
case ZEND_SEND_VAR_NO_REF:
case ZEND_SEND_VAR_NO_REF_EX:
case ZEND_SEND_REF:
case ZEND_SEND_EXPLICIT_REF:
case ZEND_SEPARATE:
case ZEND_END_SILENCE:
case ZEND_MAKE_REF:
1 change: 1 addition & 0 deletions Zend/Optimizer/zend_ssa.c
Original file line number Diff line number Diff line change
@@ -685,6 +685,7 @@ static zend_always_inline int _zend_ssa_rename_op(const zend_op_array *op_array,
case ZEND_SEND_VAR_EX:
case ZEND_SEND_FUNC_ARG:
case ZEND_SEND_REF:
case ZEND_SEND_EXPLICIT_REF:
case ZEND_SEND_UNPACK:
case ZEND_FE_RESET_RW:
case ZEND_MAKE_REF:
40 changes: 40 additions & 0 deletions Zend/tests/explicitSendByRef/__call.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
__call() magic with explicit pass by ref
--FILE--
<?php

class Incrementor {
public function inc(&$i) {
$i++;
}
}

class ForwardCalls {
private $object;
public function __construct($object) {
$this->object = $object;
}
public function __call(string $method, array $args) {
return $this->object->$method(...$args);
}
}

$forward = new ForwardCalls(new Incrementor);

$i = 0;
try {
$forward->inc(&$i);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($i);

$i = 0;
$forward->inc($i);
var_dump($i);

?>
--EXPECT--
Cannot pass reference to by-value parameter 1
int(0)
int(0)
86 changes: 86 additions & 0 deletions Zend/tests/explicitSendByRef/basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
--TEST--
Basic tests for explicit pass-by-ref
--FILE--
<?php

// Works (by-ref arg)
$a = 42;
incArgRef(&$a);
var_dump($a);

// Works (by-ref arg, deep reference)
writeArgRef(&$b[0][1]);
var_dump($b);

// Works (prefer-ref arg)
$c = 42;
$vars = ['b' => 2, 'a' => 1];
$vars2 = [2, 1];
array_multisort(&$vars, $vars2);
var_dump($vars, $vars2);

// Works (by-ref arg, by-ref function)
$e = 42;
incArgRef(&returnsRef($e));
var_dump($e);

// Fails (by-val arg)
try {
$f = 1;
var_dump(incArgVal(&$f));
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

// Fails (by-ref arg, by-val function)
try {
$g = 42;
incArgRef(&returnsVal($g));
var_dump($g);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

// Fails (by-val arg, by-ref function)
try {
$h = 1;
var_dump(incArgVal(&returnsRef($h)));
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

// Functions intentionally declared at the end of the file,
// to avoid the fbc being known during compilation
function incArgVal($a) { return $a + 1; }
function incArgRef(&$a) { $a++; }
function writeArgRef(&$a) { $a = 43; }

function returnsVal($a) { return $a; }
function &returnsRef(&$a) { return $a; }

?>
--EXPECT--
int(43)
array(1) {
[0]=>
array(1) {
[1]=>
int(43)
}
}
array(2) {
["a"]=>
int(1)
["b"]=>
int(2)
}
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
int(43)
Cannot pass reference to by-value parameter 1
Cannot pass result of by-value function by reference
Cannot pass reference to by-value parameter 1
14 changes: 14 additions & 0 deletions Zend/tests/explicitSendByRef/call_user_func.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
call_user_func() with explicit pass by ref
--FILE--
<?php

function inc(&$i) { $i++; }

$i = 0;
call_user_func('Foo\inc', &$i);
var_dump($i);

?>
--EXPECTF--
Fatal error: Cannot pass reference to by-value parameter 2 in %s on line %d
11 changes: 11 additions & 0 deletions Zend/tests/explicitSendByRef/compileError.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
A compilation error is thrown is the ref/val mismatch is detected at compile-time
--FILE--
<?php

function argByVal($a) {}
argByVal(&$b);

?>
--EXPECTF--
Fatal error: Cannot pass reference to by-value parameter 1 in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/explicitSendByRef/compileErrorForOptimizedFunction.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
A compilation error is thrown when passing result of optimized function by explicit ref
--FILE--
<?php

function argByRef(&$a) {}

argByRef(&strlen("foo"));

?>
--EXPECTF--
Fatal error: Cannot pass result of by-value function by reference in %s on line %d
52 changes: 52 additions & 0 deletions Zend/tests/explicitSendByRef/manyArgs.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
--TEST--
Test behavior with many args (slow-path)
--FILE--
<?php

$a = 42;
argByRef(
100, 101, 102, 103, 104, 105, 106, 107, 108, 109,
110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
130, 131, 132, 133, 134, 135, 136, 137, 138, 139,
&$a
);
var_dump($a);

try {
$b = 42;
argByVal(
100, 101, 102, 103, 104, 105, 106, 107, 108, 109,
110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
130, 131, 132, 133, 134, 135, 136, 137, 138, 139,
&$a
);
var_dump($b);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

function argByVal(
$a00, $a01, $a02, $a03, $a04, $a05, $a06, $a07, $a08, $a09,
$a10, $a11, $a12, $a13, $a14, $a15, $a16, $a17, $a18, $a19,
$a20, $a21, $a22, $a23, $a24, $a25, $a26, $a27, $a28, $a29,
$a30, $a31, $a32, $a33, $a34, $a35, $a36, $a37, $a38, $a39,
$val
) {
return $val + 1;
}
function argByRef(
$a00, $a01, $a02, $a03, $a04, $a05, $a06, $a07, $a08, $a09,
$a10, $a11, $a12, $a13, $a14, $a15, $a16, $a17, $a18, $a19,
$a20, $a21, $a22, $a23, $a24, $a25, $a26, $a27, $a28, $a29,
$a30, $a31, $a32, $a33, $a34, $a35, $a36, $a37, $a38, $a39,
&$ref
) {
$ref++;
}

?>
--EXPECT--
int(43)
Cannot pass reference to by-value parameter 41
11 changes: 11 additions & 0 deletions Zend/tests/explicitSendByRef/parseErrorOnNonVariable.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
Using explicit pass-by-ref with a non-variable results in a parse error
--FILE--
<?php

function byRef(&$a) {}
byRef(&null);

?>
--EXPECTF--
Parse error: syntax error, unexpected token ")", expecting "->" or "?->" or "{" or "[" in %s on line %d
31 changes: 28 additions & 3 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
@@ -3747,9 +3747,30 @@ static uint32_t zend_compile_args(
arg_count++;
}

/* Treat passing of $GLOBALS the same as passing a call.
* This will error at runtime if the argument is by-ref. */
if (zend_is_call(arg) || is_globals_fetch(arg)) {
if (arg->kind == ZEND_AST_REF) {
arg = arg->child[0];
ZEND_ASSERT(zend_is_variable_or_call(arg));

if (fbc && !ARG_SHOULD_BE_SENT_BY_REF(fbc, arg_num)) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot pass reference to by-value parameter %" PRIu32, arg_num);
}

zend_compile_var(&arg_node, arg, BP_VAR_W, 1);
if (zend_is_call(arg)) {
if (arg_node.op_type & (IS_CONST|IS_TMP_VAR)) {
/* Function call was converted into builtin instruction */
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot pass result of by-value function by reference");
}

opcode = ZEND_SEND_EXPLICIT_REF_FUNC;
} else {
opcode = fbc ? ZEND_SEND_REF : ZEND_SEND_EXPLICIT_REF;
}
} else if (zend_is_call(arg) || is_globals_fetch(arg)) {
/* Treat passing of $GLOBALS the same as passing a call.
* This will error at runtime if the argument is by-ref. */
zend_compile_var(&arg_node, arg, BP_VAR_R, 0);
if (arg_node.op_type & (IS_CONST|IS_TMP_VAR)) {
/* Function call was converted into builtin instruction */
@@ -4281,6 +4302,10 @@ static zend_result zend_compile_func_cuf(znode *result, zend_ast_list *args, zen
znode arg_node;
zend_op *opline;

if (arg_ast->kind == ZEND_AST_REF) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot pass reference to by-value parameter %" PRIu32, i + 1);
}
zend_compile_expr(&arg_node, arg_ast);

opline = zend_emit_op(NULL, ZEND_SEND_USER, &arg_node, NULL);
2 changes: 2 additions & 0 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
@@ -4394,6 +4394,8 @@ static void cleanup_unfinished_calls(zend_execute_data *execute_data, uint32_t o
case ZEND_SEND_VAR_NO_REF:
case ZEND_SEND_VAR_NO_REF_EX:
case ZEND_SEND_USER:
case ZEND_SEND_EXPLICIT_REF:
case ZEND_SEND_EXPLICIT_REF_FUNC:
if (level == 0) {
/* For named args, the number of arguments is up to date. */
if (opline->op2_type != IS_CONST) {
1 change: 1 addition & 0 deletions Zend/zend_language_parser.y
Original file line number Diff line number Diff line change
@@ -904,6 +904,7 @@ argument:
| identifier ':' expr
{ $$ = zend_ast_create(ZEND_AST_NAMED_ARG, $1, $3); }
| T_ELLIPSIS expr { $$ = zend_ast_create(ZEND_AST_UNPACK, $2); }
| ampersand variable { $$ = zend_ast_create(ZEND_AST_REF, $2); }
;

global_var_list:
68 changes: 68 additions & 0 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
@@ -4959,6 +4959,74 @@ ZEND_VM_HANDLER(67, ZEND_SEND_REF, VAR|CV, CONST|UNUSED|NUM)
ZEND_VM_NEXT_OPCODE();
}

ZEND_VM_HANDLER(209, ZEND_SEND_EXPLICIT_REF, VAR|CV, NUM, SPEC(QUICK_ARG))
{
USE_OPLINE
zval *varptr, *arg;
uint32_t arg_num = opline->op2.num;

if (EXPECTED(arg_num <= MAX_ARG_FLAG_NUM)) {
if (!QUICK_ARG_SHOULD_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
ZEND_VM_C_GOTO(invalid_send_ref);
}
} else if (!ARG_SHOULD_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
ZEND_VM_C_LABEL(invalid_send_ref):
SAVE_OPLINE();
zend_throw_error(NULL, "Cannot pass reference to by-value parameter %" PRIu32, arg_num);
FREE_OP1();
arg = ZEND_CALL_VAR(EX(call), opline->result.var);
ZVAL_UNDEF(arg);
HANDLE_EXCEPTION();
}

SAVE_OPLINE();
varptr = GET_OP1_ZVAL_PTR_PTR(BP_VAR_W);
arg = ZEND_CALL_VAR(EX(call), opline->result.var);
if (Z_ISREF_P(varptr)) {
Z_ADDREF_P(varptr);
} else {
ZVAL_MAKE_REF_EX(varptr, 2);
}
ZVAL_REF(arg, Z_REF_P(varptr));

FREE_OP1();
ZEND_VM_NEXT_OPCODE();
}

ZEND_VM_HANDLER(210, ZEND_SEND_EXPLICIT_REF_FUNC, VAR, NUM)
{
USE_OPLINE
zval *varptr, *arg;
uint32_t arg_num = opline->op2.num;

if (EXPECTED(arg_num <= MAX_ARG_FLAG_NUM)) {
if (!QUICK_ARG_SHOULD_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
ZEND_VM_C_GOTO(invalid_send_ref);
}
} else if (!ARG_SHOULD_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
ZEND_VM_C_LABEL(invalid_send_ref):
SAVE_OPLINE();
zend_throw_error(NULL, "Cannot pass reference to by-value parameter %" PRIu32, arg_num);
FREE_OP1();
arg = ZEND_CALL_VAR(EX(call), opline->result.var);
ZVAL_UNDEF(arg);
HANDLE_EXCEPTION();
}

varptr = GET_OP1_ZVAL_PTR_PTR(BP_VAR_W);
arg = ZEND_CALL_VAR(EX(call), opline->result.var);
if (UNEXPECTED(!Z_ISREF_P(varptr))) {
SAVE_OPLINE();
zend_throw_error(NULL, "Cannot pass result of by-value function by reference");
FREE_OP1();
ZVAL_UNDEF(arg);
HANDLE_EXCEPTION();
}

ZVAL_COPY_VALUE(arg, varptr);
ZEND_VM_NEXT_OPCODE();
}

ZEND_VM_HOT_SEND_HANDLER(66, ZEND_SEND_VAR_EX, VAR|CV, CONST|UNUSED|NUM, SPEC(QUICK_ARG))
{
USE_OPLINE
399 changes: 307 additions & 92 deletions Zend/zend_vm_execute.h

Large diffs are not rendered by default.

985 changes: 495 additions & 490 deletions Zend/zend_vm_handlers.h

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions Zend/zend_vm_opcodes.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Zend/zend_vm_opcodes.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.