Parentheses altering semantics of function call result

PhpPhp Internals

Php Problem Overview


It was noted in another question that wrapping the result of a PHP function call in parentheses can somehow convert the result into a fully-fledged expression, such that the following works:

<?php
error_reporting(E_ALL | E_STRICT);
 
function get_array() {
   return array();
}
 
function foo() {
   // return reset(get_array());
   //              ^ error: "Only variables should be passed by reference"
   
   return reset((get_array()));
   //           ^ OK
}
 
foo();

I'm trying to find anything in the documentation to explicitly and unambiguously explain what is happening here. Unlike in C++, I don't know enough about the PHP grammar and its treatment of statements/expressions to derive it myself.

Is there anything hidden in the documentation regarding this behaviour? If not, can somebody else explain it without resorting to supposition?


Update

I first found this EBNF purporting to represent the PHP grammar, and tried to decode my scripts myself, but eventually gave up.

Then, using phc to generate a .dot file of the two foo() variants, I produced AST images for both scripts using the following commands:

$ yum install phc graphviz
$ phc --dump-ast-dot test1.php > test1.dot
$ dot -Tpng test1.dot > test1.png
$ phc --dump-ast-dot test2.php > test2.dot
$ dot -Tpng test2.dot > test2.png

In both cases the result was exactly the same:

Parse tree of snippets 1 and 2

Php Solutions


Solution 1 - Php

This behavior could be classified as bug, so you should definitely not rely on it.

The (simplified) conditions for the message not to be thrown on a function call are as follows (see the definition of the opcode ZEND_SEND_VAR_NO_REF):

  • the argument is not a function call (or if it is, it returns by reference), and
  • the argument is either a reference or it has reference count 1 (if it has reference count 1, it's turned into a reference).

Let's analyze these in more detail.

First point is true (not a function call)

Due to the additional parentheses, PHP no longer detects that the argument is a function call.

When parsing a non empty function argument list there are three possibilities for PHP:

  • An expr_without_variable
  • A variable
  • (A & followed by a variable, for the removed call-time pass by reference feature)

When writing just get_array() PHP sees this as a variable.

(get_array()) on the other hand does not qualify as a variable. It is an expr_without_variable.

This ultimately affects the way the code compiles, namely the extended value of the opcode SEND_VAR_NO_REF will no longer include the flag ZEND_ARG_SEND_FUNCTION, which is the way the function call is detected in the opcode implementation.

Second point is true (the reference count is 1)

At several points, the Zend Engine allows non-references with reference count 1 where references are expected. These details should not be exposed to the user, but unfortunately they are here.

In your example you're returning an array that's not referenced from anywhere else. If it were, you would still get the message, i.e. this second point would not be true.

So the following very similar example does not work:

<?php

$a = array();
function get_array() {
   return $GLOBALS['a'];
}

return reset((get_array()));

Solution 2 - Php

A) To understand what's happening here, one needs to understand PHP's handling of values/variables and references (PDF, 1.2MB). As stated throughout the documentation: "references are not pointers"; and you can only return variables by reference from a function - nothing else.

In my opinion, that means, any function in PHP will return a reference. But some functions (built in PHP) require values/variables as arguments. Now, if you are nesting function-calls, the inner one returns a reference, while the outer one expects a value. This leads to the 'famous' E_STRICT-error "Only variables should be passed by reference".

$fileName = 'example.txt';
$fileExtension = array_pop(explode('.', $fileName));
// will result in Error 2048: Only variables should be passed by reference in…

B) I found a line in the PHP-syntax description linked in the question.

expr_without_variable = "(" expr ")"

In combination with this sentence from the documentation: "In PHP, almost anything you write is an expression. The simplest yet most accurate way to define an expression is 'anything that has a value'.", this leads me to the conclusion that even (5) is an expression in PHP, which evaluates to an integer with the value 5.

(As $a = 5 is not only an assignment but also an expression, which evalutes to 5.)

Conclusion

If you pass a reference to the expression (...), this expression will return a value, which then may be passed as argument to the outer function. If that (my line of thought) is true, the following two lines should work equivalently:

// what I've used over years: (spaces only added for readability)
$fileExtension = array_pop( ( explode('.', $fileName) ) );
// vs
$fileExtension = array_pop( $tmp = explode('.', $fileName) );

See also PHP 5.0.5: Fatal error: Only variables can be passed by reference; 13.09.2005

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLightness Races in OrbitView Question on Stackoverflow
Solution 1 - PhpNikiCView Answer on Stackoverflow
Solution 2 - PhpfeeelaView Answer on Stackoverflow