Skip to content

Commit f6f7444

Browse files
committed
[POC] Support calling global functions from constant expressions
- Currently, this treats all functions as global functions. Function resolution is straightforward to implement, I just haven't implemented it for a proof of concept. - Argument unpacking isn't supported, but would be easy to implement. - It turns out parameter defaults evaluate some constants every time, depending on whether the PHP value is immutable. It's likely we want to change this for expressions containing function calls, which can be more expensive than evaluating a regular constant AST. (e.g. reading files) - This handles recursive definitions. - Static calls with known classes (other than static::) may also be easy to implement. - This also constrains function return values to be valid constants, (for what define() would accept) and throws an error otherwise. In the future, `const X = [my_call()->x];` may be possible. - Variables aren't supported, because they depend on the declaration's scope, which may cease to exist. - This throws an Error if a function call in a constant begins when it's still in progress. Aside: https://github.com/TysonAndre/php-src/pull/10/files is an alternative approach to a simple syntax to declaring constants with dynamic values, which instead limits dynamic expressions of any type to `static const`. ------- It turns out that function calls can already be invoked when evaluating a constant, e.g. from php's error handler. So it seems viable for PHP's to support this, which this POC attempts to do. (Imagine an error handler defining SOME_DYNAMIC_CONST123 to be a dynamic value if a notice is emitted) for `const X = [[]['undefined_index'], SOME_DYNAMIC_CONST123][1];`
1 parent 986da2a commit f6f7444

10 files changed

+282
-2
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Can call internal functions from class constants
3+
--FILE--
4+
<?php
5+
class Example {
6+
const X = sprintf("Hello, %s\n", "World");
7+
8+
public static function main() {
9+
echo "X is " . self::X . "\n";
10+
}
11+
}
12+
Example::main();
13+
?>
14+
--EXPECT--
15+
X is Hello, World
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
Can call internal functions from class constants
3+
--FILE--
4+
<?php
5+
function normalize_keys(array $x) {
6+
// Should only invoke normalize_keys once
7+
echo "Normalizing the keys\n";
8+
$result = [];
9+
foreach ($x as $k => $value) {
10+
$result["prefix_$k"] = $value;
11+
}
12+
return $result;
13+
}
14+
class Example {
15+
const X = [
16+
'key1' => 'value1',
17+
'key2' => 'value2',
18+
];
19+
const Y = array_flip(self::X);
20+
const Z = normalize_keys(self::Y);
21+
}
22+
var_export(Example::Z);
23+
var_export(Example::Z);
24+
var_export(Example::Y);
25+
?>
26+
--EXPECT--
27+
Normalizing the keys
28+
array (
29+
'prefix_value1' => 'key1',
30+
'prefix_value2' => 'key2',
31+
)array (
32+
'prefix_value1' => 'key1',
33+
'prefix_value2' => 'key2',
34+
)array (
35+
'value1' => 'key1',
36+
'value2' => 'key2',
37+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--TEST--
2+
Can call internal functions from global constants
3+
--FILE--
4+
<?php
5+
const NIL = var_export(null, true);
6+
7+
echo "NIL is " . NIL . "\n";
8+
?>
9+
--EXPECT--
10+
NIL is NULL
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Cannot declare constants with function calls that contain objects
3+
--FILE--
4+
<?php
5+
function make_object_array() {
6+
return [new stdClass()];
7+
}
8+
const OBJECT_VALUES = make_object_array();
9+
?>
10+
--EXPECTF--
11+
Fatal error: Uncaught Error: Calls in constants may only evaluate to scalar values, arrays or resources in %s:5
12+
Stack trace:
13+
#0 {main}
14+
thrown in %s on line 5
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
Can call internal functions from parameter default
3+
--FILE--
4+
<?php
5+
function evaluated_user_function() {
6+
// NOTE: PHP only caches non-refcounted values in the RECV_INIT value,
7+
// meaning that if the returned value is dynamic, this will get called every time.
8+
// TODO: Would it be worth it to convert refcounted values to immutable values?
9+
echo "Evaluating default\n";
10+
return sprintf("%s default", "Dynamic");
11+
}
12+
function test_default($x = evaluated_user_function()) {
13+
echo "x is $x\n";
14+
}
15+
test_default();
16+
test_default(2);
17+
test_default();
18+
?>
19+
--EXPECT--
20+
Evaluating default
21+
x is Dynamic default
22+
x is 2
23+
Evaluating default
24+
x is Dynamic default
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Recursion in calls in class constants causes an error
3+
--FILE--
4+
<?php
5+
function x_plus_1() {
6+
echo "Computing X + 1\n";
7+
return Recursion::X + 1;
8+
}
9+
class Recursion {
10+
const X = x_plus_1();
11+
const MISSING = MISSING_GLOBAL + 1;
12+
}
13+
try {
14+
echo "Recursion::X=" . Recursion::X . "\n";
15+
} catch (Error $e) {
16+
printf("Caught %s: %s\n", get_class($e), $e->getMessage());
17+
}
18+
try {
19+
echo "Recursion::X=" . Recursion::X . "\n";
20+
} catch (Error $e) {
21+
printf("Second call caught %s: %s\n", get_class($e), $e->getMessage());
22+
}
23+
try {
24+
echo "Recursion::MISSING=" . Recursion::MISSING;
25+
} catch (Error $e) {
26+
printf("Caught %s: %s\n", get_class($e), $e->getMessage());
27+
}
28+
try {
29+
echo "Recursion::MISSING=" . Recursion::MISSING;
30+
} catch (Error $e) {
31+
printf("Second call caught %s: %s\n", get_class($e), $e->getMessage());
32+
}
33+
?>
34+
--EXPECT--
35+
Computing X + 1
36+
Caught Error: Unrecoverable error calling x_plus_1() in recursive constant definition
37+
Computing X + 1
38+
Second call caught Error: Unrecoverable error calling x_plus_1() in recursive constant definition
39+
Caught Error: Undefined constant 'MISSING_GLOBAL'
40+
Second call caught Error: Undefined constant 'MISSING_GLOBAL'

Zend/zend_ast.c

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@
1919

2020
#include "zend_ast.h"
2121
#include "zend_API.h"
22+
#include "zend_builtin_functions.h"
2223
#include "zend_operators.h"
2324
#include "zend_language_parser.h"
2425
#include "zend_smart_str.h"
2526
#include "zend_exceptions.h"
2627
#include "zend_constants.h"
2728

29+
/* Protection from recursive self-referencing class constants */
30+
#define IS_CONSTANT_VISITED_MARK 0x80
31+
2832
ZEND_API zend_ast_process_t zend_ast_process = NULL;
2933

3034
static inline void *zend_ast_alloc(size_t size) {
@@ -473,6 +477,37 @@ static int zend_ast_add_unpacked_element(zval *result, zval *expr) {
473477
return FAILURE;
474478
}
475479

480+
static int validate_constant_array_or_throw(HashTable *ht) /* {{{ */
481+
{
482+
int ret = 1;
483+
zval *val;
484+
485+
GC_PROTECT_RECURSION(ht);
486+
ZEND_HASH_FOREACH_VAL_IND(ht, val) {
487+
ZVAL_DEREF(val);
488+
if (Z_REFCOUNTED_P(val)) {
489+
if (Z_TYPE_P(val) == IS_ARRAY) {
490+
if (Z_REFCOUNTED_P(val)) {
491+
if (Z_IS_RECURSIVE_P(val)) {
492+
zend_throw_error(NULL, "Calls in constants cannot be recursive arrays");
493+
ret = 0;
494+
break;
495+
} else if (!validate_constant_array_or_throw(Z_ARRVAL_P(val))) {
496+
ret = 0;
497+
break;
498+
}
499+
}
500+
} else if (Z_TYPE_P(val) != IS_STRING && Z_TYPE_P(val) != IS_RESOURCE) {
501+
zend_throw_error(NULL, "Calls in constants may only evaluate to scalar values, arrays or resources");
502+
ret = 0;
503+
break;
504+
}
505+
}
506+
} ZEND_HASH_FOREACH_END();
507+
GC_UNPROTECT_RECURSION(ht);
508+
return ret;
509+
}
510+
/* }}} */
476511
ZEND_API int ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast, zend_class_entry *scope)
477512
{
478513
zval op1, op2;
@@ -720,6 +755,107 @@ ZEND_API int ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast, zend_c
720755
zval_ptr_dtor_nogc(&op2);
721756
}
722757
break;
758+
case ZEND_AST_CALL:
759+
{
760+
uint32_t i, j, arg_count;
761+
zend_string *fname;
762+
zval *func;
763+
zval *func_name;
764+
zend_ast_list *arg_list_ast;
765+
766+
zval *args;
767+
if (ast->child[0]->kind != ZEND_AST_ZVAL || Z_TYPE_P(zend_ast_get_zval(ast->child[0])) != IS_STRING) {
768+
/* Impossible */
769+
zend_throw_error(NULL, "Unsupported constant expression for function call name");
770+
return FAILURE;
771+
}
772+
func_name = zend_ast_get_zval(ast->child[0]);
773+
fname = Z_STR_P(func_name);
774+
arg_list_ast = zend_ast_get_list(ast->child[1]);
775+
arg_count = arg_list_ast->children;
776+
args = emalloc(arg_count * sizeof(zval));
777+
778+
for (i = 0; i < arg_list_ast->children; i++) {
779+
zend_ast *elem = arg_list_ast->child[i];
780+
// fprintf(stderr, "elem->kind=%d\n", (int)elem->kind);
781+
if (elem->kind == ZEND_AST_UNPACK) {
782+
zend_throw_error(NULL, "Unsupported constant expression for function call name");
783+
goto call_failure;
784+
}
785+
// XXX what is the scope
786+
if (UNEXPECTED(zend_ast_evaluate(&args[i], elem, scope) != SUCCESS)) {
787+
call_failure:
788+
for (j = 0; j < i; j++) {
789+
zval_ptr_dtor_nogc(&args[j]);
790+
}
791+
efree(args);
792+
return FAILURE;
793+
}
794+
}
795+
/* Based on INIT_FCALL - TODO: Any issues with the backtrace varying based on when this gets used? */
796+
func = zend_hash_find(EG(function_table), fname);
797+
// fprintf(stderr, "Going to call %s()\n", ZSTR_VAL(fname));
798+
if (UNEXPECTED(func == NULL)) {
799+
zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(fname));
800+
} else {
801+
zval result_copy;
802+
if (ast->attr & IS_CONSTANT_VISITED_MARK) {
803+
/* XXX should there be a way to recover from this error? */
804+
/* XXX PHP converts constants with errors to null? */
805+
zend_throw_error(NULL, "Unrecoverable error calling %s() in recursive constant definition", ZSTR_VAL(fname));
806+
return FAILURE;
807+
}
808+
ast->attr |= IS_CONSTANT_VISITED_MARK;
809+
810+
ret = call_user_function(CG(function_table), NULL, func_name, &result_copy, arg_count, args);
811+
if (EG(exception)) {
812+
ret = FAILURE;
813+
}
814+
ast->attr &= ~IS_CONSTANT_VISITED_MARK;
815+
816+
if (ret != FAILURE) {
817+
switch (Z_TYPE(result_copy)) {
818+
case IS_LONG:
819+
case IS_DOUBLE:
820+
case IS_STRING:
821+
case IS_FALSE:
822+
case IS_TRUE:
823+
case IS_NULL:
824+
case IS_RESOURCE:
825+
*result = result_copy;
826+
break;
827+
case IS_ARRAY:
828+
if (Z_REFCOUNTED_P(&result_copy)) {
829+
if (!validate_constant_array_or_throw(Z_ARRVAL(result_copy))) {
830+
ret = FAILURE;
831+
} else {
832+
/* Recursively copy arrays and replace references with values */
833+
copy_constant_array(result, &result_copy);
834+
zval_ptr_dtor(&result_copy);
835+
}
836+
} else {
837+
*result = result_copy;
838+
}
839+
break;
840+
default:
841+
zend_throw_error(NULL, "Calls in constants may only evaluate to scalar values, arrays or resources");
842+
ret = FAILURE;
843+
break;
844+
}
845+
}
846+
if (ret == FAILURE) {
847+
zval_ptr_dtor_nogc(&result_copy);
848+
}
849+
// fprintf(stderr, "retval=%d, Z_TYPE_P(result)=%d", (int)ret, (int)Z_TYPE_P(result));
850+
}
851+
for (j = 0; j < arg_count; j++) {
852+
zval_ptr_dtor_nogc(&args[j]);
853+
}
854+
efree(args);
855+
ast->attr &= ~IS_CONSTANT_VISITED_MARK;
856+
// FIXME validate that the result is a constant AST and throw if it isn't.
857+
break;
858+
}
723859
default:
724860
zend_throw_error(NULL, "Unsupported constant expression");
725861
ret = FAILURE;

Zend/zend_builtin_functions.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ static int validate_constant_array(HashTable *ht) /* {{{ */
590590
}
591591
/* }}} */
592592

593-
static void copy_constant_array(zval *dst, zval *src) /* {{{ */
593+
void copy_constant_array(zval *dst, zval *src) /* {{{ */
594594
{
595595
zend_string *key;
596596
zend_ulong idx;

Zend/zend_builtin_functions.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#define ZEND_BUILTIN_FUNCTIONS_H
2222

2323
int zend_startup_builtin_functions(void);
24+
void copy_constant_array(zval *dst, zval *src);
2425

2526
BEGIN_EXTERN_C()
2627
ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int options, int limit);

Zend/zend_compile.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8521,7 +8521,10 @@ void zend_compile_const_expr(zend_ast **ast_ptr) /* {{{ */
85218521
}
85228522

85238523
if (!zend_is_allowed_in_const_expr(ast->kind)) {
8524-
zend_error_noreturn(E_COMPILE_ERROR, "Constant expression contains invalid operations");
8524+
if (ast->kind != ZEND_AST_ARG_LIST &&
8525+
(ast->kind != ZEND_AST_CALL || (ast->child[0]->kind != ZEND_AST_ZVAL || Z_TYPE_P(zend_ast_get_zval(ast->child[0])) != IS_STRING))) {
8526+
zend_error_noreturn(E_COMPILE_ERROR, "Constant expression contains invalid operations");
8527+
}
85258528
}
85268529

85278530
switch (ast->kind) {

0 commit comments

Comments
 (0)