Skip to content

Commit 1197f8f

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 1197f8f

17 files changed

+420
-3
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
Can call user-defined functions from defaults of static properties
3+
--FILE--
4+
<?php
5+
namespace NS;
6+
7+
function log_call($arg) {
8+
echo "log_call(" . var_export($arg, true) . ")\n";
9+
return $arg;
10+
}
11+
12+
class MyClass {
13+
public static $DEBUG = log_call(true);
14+
public static $DEBUG2 = namespace\log_call(range(1,2));
15+
}
16+
echo "Start\n";
17+
var_export(MyClass::$DEBUG); echo "\n";
18+
var_export(MyClass::$DEBUG2); echo "\n";
19+
var_export(MyClass::$DEBUG); echo "\n";
20+
MyClass::$DEBUG = "New value";
21+
echo MyClass::$DEBUG . "\n";
22+
?>
23+
--EXPECT--
24+
Start
25+
log_call(true)
26+
log_call(array (
27+
0 => 1,
28+
1 => 2,
29+
))
30+
true
31+
array (
32+
0 => 1,
33+
1 => 2,
34+
)
35+
true
36+
New value
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'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Can call internal functions from defaults of static variables
3+
--FILE--
4+
<?php
5+
function main() {
6+
static $call = SPRINTF("%s!", sprintf("Hello, %s", "World"));
7+
echo "$call\n";
8+
}
9+
main();
10+
main();
11+
?>
12+
--EXPECT--
13+
Hello, World!
14+
Hello, World!
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
Can call user-defined functions from defaults of static variables
3+
--FILE--
4+
<?php
5+
function log_call(string $arg) {
6+
echo "log_call('$arg')\n";
7+
return $arg;
8+
}
9+
10+
$f = function () {
11+
static $call = log_call(sprintf("Hello, %s", "World"));
12+
echo "$call\n";
13+
};
14+
$f();
15+
$f();
16+
?>
17+
--EXPECT--
18+
log_call('Hello, World')
19+
Hello, World
20+
Hello, World
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Can call internal functions from defaults of static variables
3+
--FILE--
4+
<?php
5+
function main() {
6+
static $call = missing_sprintf("%s!", sprintf("Hello, %s", "World"));
7+
echo "$call\n";
8+
}
9+
for ($i = 0; $i < 2; $i++) {
10+
try {
11+
main();
12+
} catch (Error $e) {
13+
printf("Caught %s: %s at line %d\n", get_class($e), $e->getMessage(), $e->getLine());
14+
}
15+
}
16+
?>
17+
--EXPECT--
18+
Caught Error: Call to undefined function missing_sprintf() at line 3
19+
Caught Error: Call to undefined function missing_sprintf() at line 3

Zend/zend.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,9 @@ int zend_startup(zend_utility_functions *utility_functions) /* {{{ */
927927
#endif
928928
EG(error_reporting) = E_ALL & ~E_NOTICE;
929929

930+
CG(active_calls_in_constants) = (HashTable *)malloc(sizeof(HashTable));
931+
zend_hash_init_ex(CG(active_calls_in_constants), 128, NULL, NULL, 1, 0);
932+
930933
zend_interned_strings_init();
931934
zend_startup_builtin_functions();
932935
zend_register_standard_constants();
@@ -962,7 +965,7 @@ void zend_register_standard_ini_entries(void) /* {{{ */
962965
}
963966
/* }}} */
964967

965-
static zend_class_entry *resolve_type_name(zend_string *type_name) {
968+
static zend_class_entry *resolve_type_name(zend_string *type_name) {
966969
zend_string *lc_type_name = zend_string_tolower(type_name);
967970
zend_class_entry *ce = zend_hash_find_ptr(CG(class_table), lc_type_name);
968971

@@ -1083,6 +1086,8 @@ void zend_shutdown(void) /* {{{ */
10831086

10841087
zend_hash_destroy(GLOBAL_CONSTANTS_TABLE);
10851088
free(GLOBAL_CONSTANTS_TABLE);
1089+
zend_hash_destroy(CG(active_calls_in_constants));
1090+
free(CG(active_calls_in_constants));
10861091
zend_shutdown_strtod();
10871092

10881093
#ifdef ZTS

0 commit comments

Comments
 (0)